2012-03-16 14 views
17

sto usando Spring Validator implementazioni per convalidare il mio oggetto e vorrei sapere come si fa a scrivere un unit test per un validatore come questo:scrittura test JUnit per l'attuazione Primavera 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(); 
    } 
} 
} 

Come posso unit test CustomerValidator senza chiamare la vera implementazione di AddressValidator (prendendolo in giro)? Non ho visto nessun esempio del genere ...

In altre parole, quello che voglio veramente fare qui è prendere in giro AddressValidator che viene chiamato e instanciated all'interno del CustomerValidator ... c'è un modo per prendi in giro questo AddressValidator?

O forse lo sto guardando nel modo sbagliato? Forse quello che devo fare è prendere in giro la chiamata a ValidationUtils.invokeValidator (...), ma poi di nuovo, non sono sicuro di come fare una cosa del genere.

Lo scopo di ciò che voglio fare è davvero semplice. AddressValidator è già completamente testato in un'altra classe di test (chiamiamola th AddressValidatorTestCase). Quindi, quando scrivo la mia classe JUnit per il CustomerValidator, non voglio "re-testare" tutto da capo ... quindi voglio che AddressValidator torni sempre senza errori (attraverso ValidationUtils.invokeValidator (. ..) chiamata).

Grazie per il vostro aiuto.

EDIT (2012/03/18) - sono riuscito a trovare una buona soluzione (credo ...) utilizzando JUnit e Mockito come il quadro di scherno.

In primo luogo, la classe di test 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()); 
    } 

    // ... 
} 

L'AddressValidator è completamente testato con questa classe. Questo è il motivo per cui non voglio "ri-testare" tutto da capo in CustomerValidator. Ora, la classe di test 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()); 
    } 

    // ... 
} 

Questo è tutto. Ho trovato questa soluzione abbastanza pulita ... ma fammi sapere cosa ne pensi. È buono? È troppo complicato? Grazie per il vostro feedback.

+1

Dovresti aver creato una risposta piuttosto che modificare la domanda originale con una risposta. Quindi potresti accettare la tua risposta (se pensavi che fosse ancora la migliore). Questo è il modo normale di gestire questo scenario, credo. È sempre bene avere una risposta accettata se altre persone hanno lo stesso problema. – Dave

+0

Queste righe dovrebbero fare riferimento a customerValidator, penso, non all'indirizzoValidator. Non vedo davvero il punto nel verificare che validator.supports sia chiamato - è l'invocazione del metodo di validazione a cui sei interessato. Direi. verificare (addressValidator) .Supporto (Address.class); // verify (addressValidator) .validate (customer.getAddress(), errors); –

+0

Questo test solo il percorso "felice". Come si può verificare che un errore di AddressValidator abbia esito negativo nel CustomerValidator con errori di indirizzo anche se tutti gli altri dettagli del cliente possono essere corretti? –

risposta

31

È un test davvero semplice, senza alcuna simulazione. (Solo la creazione error-oggetto è un po 'complicato)

@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: se si vuole veramente per renderlo più complicare e rendere complicare da un finto, poi dare un'occhiata al this Blog, usano un due deride , uno per l'oggetto da testare (ok, questo è utile se non puoi crearne uno), e un secondo per l'oggetto Error (penso che sia più complicato che sia.)

+0

Grazie per la tua risposta e ... hai assolutamente ragione ... Potrei semplicemente creare una nuova istanza del mio oggetto Address e impostare tutto per renderlo valido e quindi chiamare il validatore senza simulazioni. Il fatto è ... non è esattamente quello che voglio fare qui. Sono un po 'testardo e come ho detto voglio che i miei test di validatori siano completamente indipendenti gli uni dagli altri. Sono riuscito a trovare una buona soluzione (credo ...) usando JUnit e Mockito come la struttura di derisione. (Vedi l'ultima modifica del mio post (2012/03/18)) – Fred

+0

Sto usando [Spock] (http://spockframework.org) piuttosto che JUnit, ma questo mi ha aiutato molto. Grazie! Mi ci sarebbe voluto un po 'per capire come istanziare un oggetto Error in un modo utile, e probabilmente avrei fatto molto più beffe. –

0

Ecco il codice che mostra prova come unità per la convalida:

1) La classe Validator principale per cui si ha la necessità di scrivere 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) Classe di utilità di supporto 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) Qui è il test dell'unità:

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

}