Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Writing JUnit tests for Spring Validator implementation

Tags:

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

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...

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?

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.

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).

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.

First, the AddressValidator test class:

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:

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.

like image 589
Fred Avatar asked Mar 16 '12 21:03

Fred


2 Answers

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 Error object (I think this is more complicated the it must be.)

like image 166
Ralph Avatar answered Sep 17 '22 13:09

Ralph


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:

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)

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:

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"));     }  } 
like image 43
bosco1 Avatar answered Sep 17 '22 13:09

bosco1