Подшучивание пружинного валидатора при тестировании контроллера

При написании модульных тестов посмертно для кода, созданного другим проектом, я столкнулся с вопросом о том, как смоделировать валидатор, связанный с контроллером с помощью 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.

Другие вопросы по тегам