为Spring Validator实现编写JUnit测试

Fre*_*red 21 validation junit spring mocking

我正在使用Spring 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();
    }
}
}
Run Code Online (Sandbox Code Playgroud)

如何在不调用AddressValidator的实际实现(通过模拟它)的情况下对CustomerValidator进行单元测试?我还没见过这样的例子......

换句话说,我真正想要做的是模拟在CustomerValidator中调用并实例化的AddressValidator ...有没有办法模拟这个AddressValidator?

或许我正在以错误的方式看待它?也许我需要做的是模拟对ValidationUtils.invokeValidator(...)的调用,但话又说回来,我不知道如何做这样的事情.

我想做的目的非常简单.AddressValidator已经在另一个测试类中进行了全面测试(让我们称之为AddressValidatorTestCase).因此,当我为CustomerValidator编写JUnit类时,我不想再重新测试它...所以我希望AddressValidator始终返回没有错误(通过ValidationUtils.invokeValidator(. ..)电话).

谢谢你的帮助.

编辑(2012/03/18) -我已经设法找到一个很好的解决方案(我认为...)使用JUnit和Mockito作为模拟框架.

首先,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());
    }

    // ...
}
Run Code Online (Sandbox Code Playgroud)

AddressValidator已使用此类进行了全面测试.这就是为什么我不想在CustomerValidator中再次"重新测试"它.现在,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());
    }

    // ...
}
Run Code Online (Sandbox Code Playgroud)

就是这样.我发现这个解决方案很干净......但是让我知道你的想法.好吗?太复杂了吗?感谢您的反馈意见.

Ral*_*lph 40

这是一个非常直接的测试,没有任何模拟.(只是创建错误对象有点棘手)

@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"));
}
Run Code Online (Sandbox Code Playgroud)

BTW:如果你真的想让它变得更复杂并且通过模拟使其变得复杂,那么看看这个博客,他们使用两个模拟,一个用于测试对象(好吧,如果你不能创建这个很有用)一个),和Error对象的第二个(我认为它必须是更复杂的.)