복붙노트

[SPRING] Spring Validator 구현을위한 JUnit 테스트 작성하기

SPRING

Spring Validator 구현을위한 JUnit 테스트 작성하기

내 객체를 검증하기 위해 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 클래스를 작성할 때, 다시 한번 "다시 테스트"하고 싶지는 않습니다 ... 그래서 ValidValueator를 통해 항상 AddressValidator가 반환되게하고 싶습니다. (ValidationUtils.invokeValidator (. ..) 호출).

당신의 도움을 주셔서 감사합니다.

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

    // ...
}

그게 다야. 나는이 해결책이 매우 깨끗하다는 것을 알았지 만, 당신이 생각하는 것을 알려주지. 좋은가요? 너무 복잡합니까? 귀하의 의견에 감사드립니다.

해결법

  1. ==============================

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

    BTW : 만약 당신이 정말로 더 복잡하게 만들고 그것을 모의에 의해 복잡하게 만들고 싶다면이 블로그를 보아라. 그들은 두 개의 mock을 사용한다. 하나는 테스트 할 객체를위한 것이다. (ok, 이것은 만들 수 없다면 유용하다. 하나), Error 객체에 대한 두 번째 (이것이 더 복잡 할 것입니다.)

  2. ==============================

    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"));
        }
    
    }
    
  3. from https://stackoverflow.com/questions/9744988/writing-junit-tests-for-spring-validator-implementation by cc-by-sa and MIT license