为 Spring Validator 实现编写 JUnit 测试

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/9744988/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-08 05:01:04  来源:igfitidea点击:

Writing JUnit tests for Spring Validator implementation

springjunitmockingvalidation

提问by Fred

I'm using Spring Validatorimplementations to validate my object and I would like to know how do you write a unit test for a validator like this one:

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

How can I unit test CustomerValidator without calling the real implementation of the AddressValidator (by mocking it)? I've haven't seen any example like that...

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

In other words, what I really want to do here is to mock the AddressValidator which is called and instanciated inside the CustomerValidator... is there a way to mock this AddressValidator?

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

Or maybe I'm looking at it the wrong way? Maybe what I need to do is to mock the call to ValidationUtils.invokeValidator(...), but then again, I'm not sure how to do such a thing.

或者也许我看错了?也许我需要做的是模拟对 ValidationUtils.invokeValidator(...) 的调用,但话说回来,我不知道如何做这样的事情。

The purpose of what I want to do is really simple. The AddressValidator is already fully tested in another test class (let's call it th AddressValidatorTestCase). So when I'm writing my JUnit class for the CustomerValidator, I don't want to "re-test" it all over again... so I want the AddressValidator to always return with no errors (through the ValidationUtils.invokeValidator(...) call).

我想做的事情的目的很简单。AddressValidator 已经在另一个测试类中进行了全面测试(我们称之为 AddressValidatorTestCase)。因此,当我为 CustomerValidator 编写 JUnit 类时,我不想再次“重新测试”它……所以我希望 AddressValidator 始终无错误地返回(通过 ValidationUtils.invokeValidator(. ..) 称呼)。

Thanks for your help.

谢谢你的帮助。

EDIT (2012/03/18) -I've managed to find a good solution (I think...) using JUnit and Mockito as the mocking framework.

编辑(2012 年 3 月 18 日) -我设法使用 JUnit 和 Mockito 作为模拟框架找到了一个很好的解决方案(我认为......)。

First, the AddressValidator test class:

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

    // ...
}

The AddressValidator is fully tested with this class. This is why I don't want to "re-test" it all over again in the CustomerValidator. Now, the CustomerValidator test class:

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

    // ...
}

That's about it. I found this solution pretty clean... but let me know what you think. Is it good? Is it too complicated? Thanks for your feedback.

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

回答by Ralph

It is a really straight forward test without any mock. (just the error-object creation is a bit tricky)

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

@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: if you really want to make it more complicate and make it complicate by a mock, then have a look at this Blog, they use a two mocks, one for the object to test (ok, this is useful if you can not create one), and a second for the Errorobject (I think this is more complicated the it must be.)

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

回答by bosco1

Here is the code that shows how to unit test for validation:

这是显示如何进行单元测试以进行验证的代码:

1) The main Validator class for which one needs to write unit test:

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) Utility class supporting 1)

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) Here is the unit test:

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

}