Подшучивание пружинного валидатора при тестировании контроллера
При написании модульных тестов посмертно для кода, созданного другим проектом, я столкнулся с вопросом о том, как смоделировать валидатор, связанный с контроллером с помощью initBinder
?
Обычно я хотел бы просто убедиться, что мои входные данные верны и будут выполнены с помощью нескольких дополнительных вызовов в валидаторе, но в этом случае класс валидатора сочетается с выполнением проверок через несколько источников данных, и все это становится довольно беспорядочным для тестирования. Связывание восходит к некоторым старым используемым общим библиотекам и выходит за рамки моей текущей работы по их исправлению.
Сначала я пытался просто смоделировать внешние зависимости валидатора, используя PowerMock и насмешливые статические методы, но в итоге натолкнулся на класс, который требует источника данных при его создании, и не нашел пути обхода этого.
Затем я попытался использовать обычные инструменты для макетирования, чтобы смоделировать валидатор, но это тоже не сработало. Затем попытался установить валидатор в mockMvc
звонить, но это не регистрирует больше, чем @Mock
аннотация для валидатора. Наконец столкнулся с этим вопросом. Но так как нет поля validator
на самом контроллере это тоже не получается. Итак, как я могу это исправить, чтобы работать?
Оценщик:
public class TerminationValidator implements Validator {
// JSR-303 Bean Validator utility which converts ConstraintViolations to Spring's BindingResult
private CustomValidatorBean validator = new CustomValidatorBean();
private Class<? extends Default> level;
public TerminationValidator(Class<? extends Default> level) {
this.level = level;
validator.afterPropertiesSet();
}
public boolean supports(Class<?> clazz) {
return Termination.class.equals(clazz);
}
@Override
public void validate(Object model, Errors errors) {
BindingResult result = (BindingResult) errors;
// Check domain object against JSR-303 validation constraints
validator.validate(result.getTarget(), result, this.level);
[...]
}
[...]
}
контроллер:
public class TerminationController extends AbstractController {
@InitBinder("termination")
public void initBinder(WebDataBinder binder, HttpServletRequest request) {
binder.setValidator(new TerminationValidator(Default.class));
binder.setAllowedFields(new String[] { "termId[**]", "terminationDate",
"accountSelection", "iban", "bic" });
}
[...]
}
Тестовый класс:
@RunWith(MockitoJUnitRunner.class)
public class StandaloneTerminationTests extends BaseControllerTest {
@Mock
private TerminationValidator terminationValidator = new TerminationValidator(Default.class);
@InjectMocks
private TerminationController controller;
private MockMvc mockMvc;
@Override
@Before
public void setUp() throws Exception {
initMocks(this);
mockMvc = standaloneSetup(controller)
.setCustomArgumentResolvers(new TestHandlerMethodArgumentResolver())
.setValidator(terminationValidator)
.build();
ReflectionTestUtils.setField(controller, "validator", terminationValidator);
when(terminationValidator.supports(any(Class.class))).thenReturn(true);
doNothing().when(terminationValidator).validate(any(), any(Errors.class));
}
[...]
}
Исключение:
java.lang.IllegalArgumentException: Could not find field [validator] of type [null] on target [my.application.web.controller.TerminationController@560508be]
at org.springframework.test.util.ReflectionTestUtils.setField(ReflectionTestUtils.java:111)
at org.springframework.test.util.ReflectionTestUtils.setField(ReflectionTestUtils.java:84)
at my.application.web.controller.termination.StandaloneTerminationTests.setUp(StandaloneTerminationTests.java:70)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:24)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
at org.mockito.internal.runners.JUnit45AndHigherRunnerImpl.run(JUnit45AndHigherRunnerImpl.java:37)
at org.mockito.runners.MockitoJUnitRunner.run(MockitoJUnitRunner.java:62)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
2 ответа
Вы должны избегать создания бизнес-объектов с new
в приложении Spring. Вы всегда должны получать их из контекста приложения - это облегчит насмешку в вашем тесте.
В вашем случае вы должны просто создать свой валидатор как бин (скажем, defaultTerminationValidator
) и введите его в свой контроллер:
public class TerminationController extends AbstractController {
private TerminationValidator terminationValidator;
@Autowired
public setDefaultTerminationValidator(TerminationValidator validator) {
this.terminationValidator = validator;
}
@InitBinder("termination")
public void initBinder(WebDataBinder binder, HttpServletRequest request) {
binder.setValidator(terminationValidator);
binder.setAllowedFields(new String[] { "termId[**]", "terminationDate",
"accountSelection", "iban", "bic" });
}
[...]
}
Таким образом, вы сможете просто ввести макет в ваш тест.
Ну, единственный способ, которым я знаю, - справляться с этими ситуациями, не меняя код приложения, используя PowerMock.
Он может работать с JVM и создавать макеты не только для статических методов, но и при вызове new
оператор.
Взгляните на этот пример:
https://code.google.com/p/powermock/wiki/MockConstructor
Если вы хотите использовать Mockito, вы должны использовать PowerMockito вместо PowerMock:
https://code.google.com/p/powermock/wiki/MockitoUsage13
Читать раздел How to mock construction of new objects
Например:
Мой кастомный контроллер
public class MyController {
public String doSomeStuff(String parameter) {
getValidator().validate(parameter);
// Perform other operations
return "nextView";
}
public CoolValidator getValidator() {
//Bad design, it's better to inject the validator or a factory that provides it
return new CoolValidator();
}
}
Мой пользовательский валидатор
public class CoolValidator {
public void validate(String input) throws InvalidParameterException {
//Do some validation. This code will be mocked by PowerMock!!
}
}
Мой пользовательский тест с использованием PowerMockito
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import static org.powermock.api.mockito.PowerMockito.*;
@RunWith(PowerMockRunner.class)
@PrepareForTest(MyController.class)
public class MyControllerTest {
@Test(expected=InvalidParameterException.class)
public void test() throws Exception {
whenNew(CoolValidator.class).withAnyArguments()
.thenThrow(new InvalidParameterException("error message"));
MyController controller = new MyController();
controller.doSomeStuff("test"); // this method does a "new CoolValidator()" inside
}
}
Maven зависимости
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>1.6.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>1.6.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.10.19</version>
<scope>test</scope>
</dependency>
Как вы можете видеть из моего теста, я высмеиваю поведение валидатора, поэтому оно вызывает исключение, когда контроллер вызывает его.
Однако использование PowerMock обычно означает плохой дизайн. Он должен использоваться обычно, когда вам нужно протестировать устаревшее приложение.
Если вы можете изменить приложение, лучше измените код, чтобы его можно было протестировать без использования JVM.