Написание тестов JUnit для Spring реализации валидатора
Я использую Spring Validator для проверки моего объекта, и я хотел бы знать, как вы пишете unit test для валидатора, подобного этому:
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();
}
}
}
Как я могу unit test CustomerValidator без вызова реальной реализации AddressValidator (путем насмешивания)? Я не видел такого примера...
Другими словами, я действительно хочу сделать это, чтобы издеваться над AddressValidator, который вызывается и инициализируется внутри CustomerValidator... есть ли способ издеваться над этим AddressValidator?
Или, может быть, я смотрю на это неправильно? Возможно, мне нужно сделать это, чтобы высмеять вызов ValidationUtils.invokeValidator(...), но опять же, я не уверен, как это сделать.
Цель того, что я хочу сделать, очень проста. AddressValidator уже полностью протестирован в другом тестовом классе (пусть он вызывает его AddressValidatorTestCase). Поэтому, когда я пишу свой класс JUnit для ClientValidator, я не хочу "повторно тестировать" его снова и снова... поэтому я хочу, чтобы 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 полностью протестирована с этим классом. Вот почему я не хочу "повторно проверять" его снова в ClientValidator. Теперь тестовый класс 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
Это действительно прямой тест без макета. (просто создание объекта ошибки немного сложно)
@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"));
}
Кстати: если вы действительно хотите сделать его более сложным и усложнить его макетом, посмотрите на этот блог, они используйте два mocks, один для объекта для проверки (хорошо, это полезно, если вы не можете его создать), а второй для объекта Error
(я думаю, что это сложнее, он должен быть.)
Ответ 2
Вот код, который показывает, как unit test для проверки:
1) Основной класс Validator, для которого нужно написать 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) Класс полезности, поддерживающий 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) Вот 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"));
}
}