2012-03-16 43 views
17

Tôi đang sử dụng Spring Validator triển khai để xác nhận đối tượng của tôi và tôi muốn biết làm thế nào để bạn viết một bài kiểm tra đơn vị cho một validator như thế này:Viết thử nghiệm JUnit để thực hiện mùa xuân Validator

public class CustomerValidator implements Validator { 

private final Validator addressValidator; 

public CustomerValidator(Validator addressValidator) { 
    if (addressValidator == null) { 
     throw new IllegalArgumentException(
      "The supplied [Validator] is required and must not be null."); 
    } 
    if (!addressValidator.supports(Address.class)) { 
     throw new IllegalArgumentException(
      "The supplied [Validator] must support the validation of [Address] instances."); 
    } 
    this.addressValidator = addressValidator; 
} 

/** 
* This Validator validates Customer instances, and any subclasses of Customer too 
*/ 
public boolean supports(Class clazz) { 
    return Customer.class.isAssignableFrom(clazz); 
} 

public void validate(Object target, Errors errors) { 
    ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required"); 
    ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required"); 
    Customer customer = (Customer) target; 
    try { 
     errors.pushNestedPath("address"); 
     ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors); 
    } finally { 
     errors.popNestedPath(); 
    } 
} 
} 

Làm thế nào tôi có thể đơn vị kiểm tra CustomerValidator mà không cần gọi thực hiện thực tế của AddressValidator (bằng cách chế nhạo nó)? Tôi đã không thấy bất kỳ ví dụ nào như vậy ...

Nói cách khác, những gì tôi thực sự muốn làm ở đây là giả lập AddressValidator được gọi và instanciated bên trong CustomerValidator ... có cách nào để giả lập AddressValidator này?

Hoặc có thể tôi đang xem sai đường? Có lẽ những gì tôi cần làm là để giả mạo các cuộc gọi đến ValidationUtils.invokeValidator (...), nhưng sau đó một lần nữa, tôi không chắc chắn làm thế nào để làm một điều như vậy.

Mục đích của những gì tôi muốn làm thực sự đơn giản. AddressValidator đã được kiểm tra đầy đủ trong một lớp thử nghiệm khác (hãy gọi nó là AddressValidatorTestCase). Vì vậy, khi tôi đang viết lớp JUnit của tôi cho CustomerValidator, tôi không muốn "kiểm tra lại" nó trên một lần nữa ... vì vậy tôi muốn AddressValidator luôn luôn trở lại mà không có lỗi (thông qua ValidationUtils.invokeValidator (. ..) gọi điện).

Cảm ơn sự giúp đỡ của bạn.

EDIT (2012/03/18) - Tôi đã tìm được giải pháp tốt (tôi nghĩ ...) bằng cách sử dụng JUnit và Mockito làm khung mocking.

Thứ nhất, lớp thử nghiệm AddressValidator:

public class Address { 
    private String city; 
    // ... 
} 

public class AddressValidator implements org.springframework.validation.Validator { 

    public boolean supports(Class<?> clazz) { 
     return Address.class.equals(clazz); 
    } 

    public void validate(Object obj, Errors errors) { 
     Address a = (Address) obj; 

     if (a == null) { 
      // A null object is equivalent to not specifying any of the mandatory fields 
      errors.rejectValue("city", "msg.address.city.mandatory"); 
     } else { 
      String city = a.getCity(); 

      if (StringUtils.isBlank(city)) { 
      errors.rejectValue("city", "msg.address.city.mandatory"); 
      } else if (city.length() > 80) { 
      errors.rejectValue("city", "msg.address.city.exceeds.max.length"); 
      } 
     } 
    } 
} 

public class AddressValidatorTest { 
    private Validator addressValidator; 

    @Before public void setUp() { 
     validator = new AddressValidator(); 
    } 

    @Test public void supports() { 
     assertTrue(validator.supports(Address.class)); 
     assertFalse(validator.supports(Object.class)); 
    } 

    @Test public void addressIsValid() { 
     Address address = new Address(); 
     address.setCity("Whatever"); 
     BindException errors = new BindException(address, "address"); 
     ValidationUtils.invokeValidator(validator, address, errors); 
     assertFalse(errors.hasErrors()); 
    } 

    @Test public void cityIsNull() { 
     Address address = new Address(); 
     address.setCity(null); // Already null, but only to be explicit here... 
     BindException errors = new BindException(address, "address"); 
     ValidationUtils.invokeValidator(validator, address, errors); 
     assertTrue(errors.hasErrors()); 
     assertEquals(1, errors.getFieldErrorCount("city")); 
     assertEquals("msg.address.city.mandatory", errors.getFieldError("city").getCode()); 
    } 

    // ... 
} 

Các AddressValidator được kiểm tra đầy đủ với lớp này. Đây là lý do tại sao tôi không muốn "kiểm tra lại" nó một lần nữa trong CustomerValidator. Bây giờ, lớp kiểm tra CustomerValidator:

public class Customer { 
    private String firstName; 
    private Address address; 
    // ... 
} 

public class CustomerValidator implements org.springframework.validation.Validator { 
    // See the first post above 
} 

@RunWith(MockitoJUnitRunner.class) 
public class CustomerValidatorTest { 

    @Mock private Validator addressValidator; 

    private Validator customerValidator; // Validator under test 

    @Before public void setUp() { 
     when(addressValidator.supports(Address.class)).thenReturn(true); 
     customerValidator = new CustomerValidator(addressValidator); 
     verify(addressValidator).supports(Address.class); 

     // DISCLAIMER - Here, I'm resetting my mock only because I want my tests to be completely independents from the 
     // setUp method 
     reset(addressValidator); 
    } 

    @Test(expected = IllegalArgumentException.class) 
    public void constructorAddressValidatorNotSupplied() { 
     customerValidator = new CustomerValidator(null); 
     fail(); 
    } 

    // ... 

    @Test public void customerIsValid() { 
     Customer customer = new Customer(); 
     customer.setFirstName("John"); 
     customer.setAddress(new Address()); // Don't need to set any fields since it won't be tested 

     BindException errors = new BindException(customer, "customer"); 

     when(addressValidator.supports(Address.class)).thenReturn(true); 
     // No need to mock the addressValidator.validate method since according to the Mockito documentation, void 
     // methods on mocks do nothing by default! 
     // doNothing().when(addressValidator).validate(customer.getAddress(), errors); 

     ValidationUtils.invokeValidator(customerValidator, customer, errors); 

     verify(addressValidator).supports(Address.class); 
     // verify(addressValidator).validate(customer.getAddress(), errors); 

     assertFalse(errors.hasErrors()); 
    } 

    // ... 
} 

Đó là về nó. Tôi thấy giải pháp này khá sạch sẽ ... nhưng hãy cho tôi biết suy nghĩ của bạn. Liệu nó có tốt không? Nó có quá phức tạp không? Cảm ơn phản hồi của bạn.

+1

Bạn nên tạo câu trả lời thay vì sau đó chỉnh sửa câu hỏi gốc bằng câu trả lời. Sau đó, bạn có thể chấp nhận câu trả lời của bạn (nếu bạn nghĩ rằng nó vẫn là tốt nhất). Đó là cách bình thường để xử lý kịch bản này mà tôi tin. Nó luôn luôn tốt để có một câu trả lời được chấp nhận nếu những người khác có cùng một vấn đề. – Dave

+0

Những dòng này nên tham khảo customerValidator tôi nghĩ, không phải là addressValidator. Tôi không thực sự thấy điểm trong việc xác minh rằng validator.supports được gọi - đó là lời gọi phương thức hợp lệ mà bạn quan tâm. Tôi sẽ nói. xác minh (addressValidator) .supports (Address.class); // verify (addressValidator) .validate (customer.getAddress(), errors); –

+0

Điều này chỉ kiểm tra đường dẫn "hạnh phúc". Làm thế nào bạn có thể kiểm tra rằng một thất bại AddressValidator làm cho CustomerValidator thất bại với lỗi địa chỉ mặc dù tất cả các chi tiết khách hàng khác có thể là chính xác? –

Trả lời

31

Đây thực sự là một thử nghiệm thẳng về phía trước mà không có bất kỳ mô hình nào. (Chỉ việc tạo ra lỗi đối tượng là một chút khôn lanh)

@Test 
public void testValidationWithValidAddress() { 
    AdressValidator addressValidator = new AddressValidator(); 
    CustomValidator validatorUnderTest = new CustomValidator(adressValidator); 

    Address validAddress = new Address(); 
    validAddress.set... everything to make it valid 

    Errors errors = new BeanPropertyBindingResult(validAddress, "validAddress"); 
    validatorUnderTest.validate(validAddress, errors); 

    assertFalse(errors.hasErrors()); 
} 


@Test 
public void testValidationWithEmptyFirstNameAddress() { 
    AdressValidator addressValidator = new AddressValidator(); 
    CustomValidator validatorUnderTest = new CustomValidator(adressValidator); 

    Address validAddress = new Address(); 
    invalidAddress.setFirstName("") 
    invalidAddress.set... everything to make it valid exept the first name 

    Errors errors = new BeanPropertyBindingResult(invalidAddress, "invalidAddress"); 
    validatorUnderTest.validate(invalidAddress, errors); 

    assertTrue(errors.hasErrors()); 
    assertNotNull(errors.getFieldError("firstName")); 
} 

BTW: nếu bạn thực sự muốn làm cho nó thêm phức tạp và làm cho nó phức tạp bởi một mô hình, sau đó có một cái nhìn tại this Blog, họ sử dụng một hai mocks , một cho đối tượng để kiểm tra (ok, điều này là hữu ích nếu bạn không thể tạo ra một), và một giây cho đối tượng Error (tôi nghĩ rằng điều này là phức tạp hơn nó phải được.)

+0

Cảm ơn câu trả lời của bạn và ... bạn hoàn toàn đúng ... Tôi chỉ đơn giản có thể tạo một thể hiện mới của đối tượng Địa chỉ của tôi và đặt mọi thứ để làm cho nó hợp lệ và sau đó gọi trình xác thực mà không có bất kỳ mô hình nào. Vấn đề là ... đó không phải là điều tôi muốn làm ở đây. Tôi hơi bướng bỉnh và giống như tôi đã nói tôi muốn những người kiểm chứng của tôi kiểm tra các lớp học hoàn toàn độc lập với nhau. Tôi đã tìm được một giải pháp tốt (tôi nghĩ ...) bằng cách sử dụng JUnit và Mockito làm khung mocking. (Xem bài chỉnh sửa cuối cùng của bài đăng của tôi (2012/03/18)) – Fred

+0

Tôi đang sử dụng [Spock] (http://spockframework.org) thay vì JUnit, nhưng điều này đã giúp tôi rất nhiều. Cảm ơn! Nó sẽ đưa tôi một thời gian để tìm hiểu làm thế nào để nhanh chóng một đối tượng Error một cách hữu ích, và tôi có lẽ sẽ làm nhiều hơn nhạo báng. –

0

Đây là đoạn mã cho thấy thử nghiệm làm thế nào để đơn vị để xác thực:

1) Lớp Validator chính mà cần một viết đơn vị kiểm tra:

public class AddAccountValidator implements Validator { 

    private static Logger LOGGER = Logger.getLogger(AddAccountValidator.class); 

    public boolean supports(Class clazz) { 
     return AddAccountForm.class.equals(clazz); 
    } 

    public void validate(Object command, Errors errors) { 
     AddAccountForm form = (AddAccountForm) command; 
     validateFields(form, errors); 
    } 

    protected void validateFields(AddAccountForm form, Errors errors) { 
     if (!StringUtils.isBlank(form.getAccountname()) && form.getAccountname().length()>20){ 
      LOGGER.info("Account Name is too long"); 
      ValidationUtils.rejectValue(errors, "accountName", ValidationUtils.TOOLONG_VALIDATION); 
     } 
    } 
} 

2) lớp Utility hỗ trợ 1)

public class ValidationUtils { 
    public static final String TOOLONG_VALIDATION = "toolong"; 

    public static void rejectValue(Errors errors, String fieldName, String value) { 
     if (errors.getFieldErrorCount(fieldName) == 0){ 
      errors.rejectValue(fieldName, value); 
     } 
    } 
} 

3) đây là bài kiểm tra đơn vị:

import static org.junit.Assert.assertEquals; 
import static org.junit.Assert.assertNull; 

import org.junit.Test; 
import org.springframework.validation.BeanPropertyBindingResult; 
import org.springframework.validation.Errors; 

import com.bos.web.forms.AddAccountForm; 

public class AddAccountValidatorTest { 

    @Test 
    public void validateFieldsTest_when_too_long() { 
     // given 
     AddAccountValidator addAccountValidator = new AddAccountValidator(); 
     AddAccountForm form = new AddAccountForm(); 
     form.setAccountName(
       "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1"); 

     Errors errors = new BeanPropertyBindingResult(form, ""); 

     // when 
     addAccountValidator.validateFields(form, errors); 

     // then 
     assertEquals(
       "Field error in object '' on field 'accountName': rejected value [aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1]; codes [toolong.accountName,toolong.java.lang.String,toolong]; arguments []; default message [null]", 
       errors.getFieldError("accountName").toString()); 
    } 

    @Test 
    public void validateFieldsTest_when_fine() { 
     // given 
     AddAccountValidator addAccountValidator = new AddAccountValidator(); 
     AddAccountForm form = new AddAccountForm(); 
     form.setAccountName("aaa1"); 
     Errors errors = new BeanPropertyBindingResult(form, ""); 

     // when 
     addAccountValidator.validateFields(form, errors); 

     // then 
     assertNull(errors.getFieldError("accountName")); 
    } 

} 
Các vấn đề liên quan