2012-03-16 2 views
17

Я использую Spring Validator реализацию, чтобы проверить мой объект, и я хотел бы знать, как вы пишете модульное тестирование для проверки правильности, как этот:Написание тестов JUnit для реализации Spring 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(); 
    } 
} 
} 

Как я могу единичный тест CustomerValidator без вызова реальной реализации AddressValidator (путем издевательства)? Я не видел такого примера ...

Другими словами, я действительно хочу сделать это, чтобы издеваться над AddressValidator, который вызывается и инициализируется внутри CustomerValidator ... есть ли способ высмеивать это AddressValidator?

Или, может быть, я смотрю на это неправильно? Возможно, мне нужно сделать это, чтобы высмеять вызов ValidationUtils.invokeValidator (...), но опять же, я не уверен, как это сделать.

Целью того, что я хочу сделать, очень просто. AddressValidator уже полностью протестирован в другом тестовом классе (назовем его th 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

Вы должны были создать ответ, а затем отредактировать исходный вопрос с ответом. Тогда вы могли бы принять свой ответ (если бы вы думали, что это все еще лучше). Я считаю, что это обычный способ справиться с этим сценарием. Всегда хорошо иметь принятый ответ, если у других людей такая же проблема. – Dave

+0

Эти строки должны ссылаться на customerValidator, я думаю, а не на адресValidator. Я действительно не вижу смысла в проверке того, что validator.supports вызывается - это вызов метода validate, который вас интересует. Я бы сказал. проверить (addressValidator) .supports (Address.class); // проверить (адресValidator) .validate (customer.getAddress(), ошибки); –

+0

Это проверяет только «счастливый» путь. Как вы можете проверить, что сбой AddressValidator приводит к сбою ClientValidator с ошибками адреса, хотя все остальные данные клиента могут быть правильными? –

ответ

31

Это действительно прямой тест без каких-либо макетов. (Только создание ошибок объект является немного сложнее)

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

КСТАТИ: если вы действительно хотите, чтобы сделать его более усложнить и сделать это усложнит на макет, а затем посмотреть на this Blog, они используют два издевается , один для объекта для тестирования (хорошо, это полезно, если вы не можете его создать), а второй для объекта Error (я думаю, что это сложнее, он должен быть.)

+0

Спасибо за ваш ответ и ... вы абсолютно правы ... Я мог бы просто создать новый экземпляр моего объекта Address и установить все, чтобы сделать его действительным, а затем вызвать валидатор без макета. Вещь ... это не совсем то, что я хочу здесь сделать. Я немного упрям, и, как я сказал, я хочу, чтобы мои тесты тестов валидаторов были полностью независимыми друг от друга. Мне удалось найти хорошее решение (я думаю ...), используя JUnit и Mockito в качестве насмешливой структуры. (См. Последнее изменение моего сообщения (2012/03/18)) – Fred

+0

Я использую [Spock] (http://spockframework.org), а не JUnit, но это очень помогло мне. Благодаря! Мне потребовалось некоторое время, чтобы выяснить, как создать экземпляр объекта Error полезным образом, и я, вероятно, сделал бы намного более насмешливым. –

0

Вот код, который показывает испытание, как блок для проверки:

1) Основной класс средства проверки, для которого нужно написать модульный тест:

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) Здесь тестовый модуль:

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

} 
Смежные вопросы