2012-03-16 23 views
17

estoy usando Spring Validator implementaciones para validar mi objetivo y me gustaría saber cómo se escribe una prueba unitaria para un validador como éste:escribir pruebas JUnit para la implementación de Primavera Validador

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

¿Cómo puedo prueba la unidad CustomerValidator sin llamar a la implementación real de AddressValidator (burlándose de ella)? No he visto ningún ejemplo como ese ...

En otras palabras, lo que realmente quiero hacer aquí es simular el AddressValidator que se llama y se instancia en el CustomerValidator ... ¿hay alguna manera de ¿se burla de este AddressValidator?

¿O quizás lo estoy viendo de la manera incorrecta? Tal vez lo que tengo que hacer es burlarme de la llamada a ValidationUtils.invokeValidator (...), pero, nuevamente, no estoy seguro de cómo hacer tal cosa.

El propósito de lo que quiero hacer es realmente simple. El AddressValidator ya está completamente probado en otra clase de prueba (llamémoslo th AddressValidatorTestCase). Así que cuando estoy escribiendo mi clase JUnit para el CustomerValidator, no quiero volver a probarlo todo ... por lo que quiero que el AddressValidator siempre regrese sin errores (a través de ValidationUtils.invokeValidator (. ..) llamada).

Gracias por su ayuda.

EDITAR (2012/03/18) - He logrado encontrar una buena solución (creo ...) utilizando JUnit y Mockito como marco de burla.

En primer lugar, la clase de prueba 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()); 
    } 

    // ... 
} 

El AddressValidator se prueba completamente con esta clase. Es por eso que no quiero volver a probarlo todo en el CustomerValidator. Ahora, la clase de prueba 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()); 
    } 

    // ... 
} 

Eso es todo. Encontré esta solución bastante limpia ... pero déjame saber lo que piensas. ¿Esta bien? ¿Es demasiado complicado? Gracias por su respuesta.

+1

Debería haber creado una respuesta en lugar de editar la pregunta original con una respuesta. Entonces podrías aceptar tu respuesta (si crees que todavía es la mejor). Esa es la forma normal de manejar este escenario, creo. Siempre es bueno tener una respuesta aceptada si otras personas tienen el mismo problema. – Dave

+0

Estas líneas deberían referirse al customerValidator creo, no al addressValidator. Realmente no veo el punto de verificar que se llame a validator.supports, es la invocación al método de validación que le interesa. Yo diría. verify (addressValidator) .supports (Address.class); // verify (addressValidator) .validate (customer.getAddress(), errors); –

+0

Esto solo prueba la ruta "feliz". ¿Cómo se puede probar que una falla de AddressValidator hace que CustomerValidator falle con errores de dirección a pesar de que todos los demás datos del cliente pueden ser correctos? –

Respuesta

31

Es una prueba muy directa sin ningún simulacro. (Sólo la creación de error a objetos es un poco complicado)

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

Por cierto: si realmente desea hacerlo más complicar y hacer que complican por una maqueta, a continuación, echar un vistazo a this Blog, utilizan unas dos simulacros , uno para el objeto a probar (vale, esto es útil si no puedes crear uno), y un segundo para el objeto Error (creo que esto es más complicado que debe ser).)

+0

Gracias por su respuesta y ... tiene toda la razón ... Podría simplemente crear una nueva instancia de mi objeto de dirección y configurar todo para que sea válida y luego llamar al validador sin ningún simulacro. El asunto es ... eso no es exactamente lo que quiero hacer aquí. Soy un poco terco y como dije, quiero que mis validadores prueben que las clases sean completamente independientes entre sí. He logrado encontrar una buena solución (creo ...) usando JUnit y Mockito como el marco de burla. (Ver la última edición de mi publicación (2012/03/18)) – Fred

+0

Estoy usando [Spock] (http://spockframework.org) en lugar de JUnit, pero esto me ayudó mucho. ¡Gracias! Me habría llevado un tiempo descubrir cómo crear una instancia de un objeto Error de una manera útil, y probablemente me habría burlado mucho más. –

0

Este es el código que muestra la prueba de cómo la unidad de validación:

1) La clase Validator principal para el que se necesita para escribir pruebas unitarias:

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) clase de utilidad de soporte 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) Aquí es la prueba de la unidad:

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

} 
Cuestiones relacionadas