【问题标题】:Writing JUnit tests for Spring Validator implementation为 Spring Validator 实现编写 JUnit 测试
【发布时间】:2012-04-02 10:50:47
【问题描述】:

我正在使用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();
    }
}
}

如何在不调用 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());
    }

    // ...
}

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

    // ...
}

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

【问题讨论】:

  • 您应该创建一个答案,而不是用答案编辑原始问题。然后你可以接受你的答案(如果你认为它仍然是最好的)。我相信这是处理这种情况的正常方式。如果其他人有同样的问题,有一个公认的答案总是好的。
  • 我认为这些行应该指的是customerValidator,而不是addressValidator。我真的看不出验证是否调用了 validator.supports 的意义——这是您感兴趣的 validate 方法调用。我会说。验证(addressValidator).supports(Address.class); // verify(addressValidator).validate(customer.getAddress(), errors);
  • 这仅测试“快乐”路径。即使所有其他客户详细信息可能正确,您如何测试 AddressValidator 失败是否会导致 CustomerValidator 失败并出现地址错误?

标签: spring junit mocking validation


【解决方案1】:

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

@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"));
}

顺便说一句:如果您真的想通过模拟使其更复杂并使其复杂化,请查看this Blog,他们使用两个模拟,一个用于测试对象(好的,这很有用,如果你不能创建一个),第二个用于Error 对象(我认为这一定更复杂。)

【讨论】:

  • 感谢您的回答,并且...您绝对正确...我可以简单地创建我的 Address 对象的新实例并设置所有内容以使其有效,然后调用验证器而无需任何模拟.问题是……这不是我想在这里做的。我有点固执,就像我说的我希望我的验证器测试类彼此完全独立。我已经设法使用 JUnit 和 Mockito 作为模拟框架找到了一个很好的解决方案(我认为......)。 (请参阅我的帖子的最后编辑(2012/03/18))
  • 我使用的是 Spock 而不是 JUnit,但这对我帮助很大。谢谢!我可能会花一些时间来弄清楚如何以一种有用的方式实例化一个 Error 对象,而且我可能会做更多的模拟。
【解决方案2】:

以下代码展示了如何对验证进行单元测试:

1) 需要为其编写单元测试的主要 Validator 类:

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) 支持 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) 这里是单元测试:

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"));
    }

}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-01-14
    • 1970-01-01
    • 1970-01-01
    • 2018-11-27
    • 2014-03-19
    • 2016-01-06
    • 2021-05-17
    相关资源
    最近更新 更多