web-dev-qa-db-fra.com

Écriture de tests JUnit pour l'implémentation Spring Validator

J'utilise Spring Validator implémentations pour valider mon objet et je voudrais savoir comment vous écrivez un test unitaire pour un validateur comme celui-ci:

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

Comment est-ce que je peux tester CustomerValidator sans appeler l'implémentation réelle de AddressValidator (en se moquant de lui)? Je n'ai jamais vu d'exemple comme ça ...

En d'autres termes, ce que je veux vraiment faire ici est de se moquer de AddressValidator qui est appelé et instancié dans CustomerValidator ... y a-t-il un moyen de se moquer de ce AddressValidator?

Ou peut-être que je le regarde de la mauvaise façon? Peut-être que ce que je dois faire est de se moquer de l'appel à ValidationUtils.invokeValidator (...), mais là encore, je ne suis pas sûr de savoir comment faire une telle chose.

Le but de ce que je veux faire est très simple. AddressValidator est déjà entièrement testé dans une autre classe de test (appelons-le AddressValidatorTestCase). Ainsi, lorsque j'écris ma classe JUnit pour CustomerValidator, je ne veux pas "le tester à nouveau" ... donc je veux que AddressValidator revienne toujours sans erreur (via ValidationUtils.invokeValidator (. ..) appel).

Merci de votre aide.

EDIT (2012/03/18) - J'ai réussi à trouver une bonne solution (je pense ...) en utilisant JUnit et Mockito comme framework moqueur.

Tout d'abord, la classe de 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());
    }

    // ...
}

AddressValidator est entièrement testé avec cette classe. C'est pourquoi je ne veux pas "tester à nouveau" tout dans le CustomerValidator. Maintenant, la classe de 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());
    }

    // ...
}

C'est à peu près ça. J'ai trouvé cette solution plutôt propre ... mais dites-moi ce que vous en pensez. Est-ce bien? Est-ce trop compliqué? Merci pour vos commentaires.

19
Fred

C'est un test vraiment simple sans aucune maquette. (juste la création d'objet d'erreur est un peu délicate)

@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: si vous voulez vraiment le rendre plus compliqué et le rendre compliqué par un simulacre, alors regardez ce Blog , ils utilisent deux simulacres, un pour l'objet à tester vous ne pouvez pas en créer un), et un second pour l’objet Error (je pense que c’est plus compliqué que ça doit être.)

37
Ralph

Voici le code qui montre comment effectuer des tests unitaires pour la validation:

1) La classe principale du Validateur pour laquelle il est nécessaire d’écrire le test unitaire:

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 utilitaire supportant 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) Voici le test unitaire:

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

}
0
bosco1