Отказывание валидатора Spring при модульном тестировании Контроллер

Во время написания модульных тестов postmortem для кода, созданного другим проектом, я столкнулся с этой проблемой, как издеваться над валидатором, связанным с контроллером с помощью initBinder?

Обычно я просто хотел бы убедиться, что мои входы действительны и выполняются с несколькими дополнительными вызовами в валидаторе, но в этом случае класс валидатора связан с выполнением проверок через несколько источников данных, и все это становится довольно беспорядочным тестировать. Связывание датируется некоторыми старыми распространенными библиотеками и выходит за рамки моей текущей работы, чтобы исправить их все.

Сначала я пытался просто издеваться над внешними зависимостями валидатора с помощью PowerMock и издеваться над статическими методами, но в конечном итоге столкнулся с классом, который требует источника данных при создании класса и не нашел пути вокруг этого.

Затем я попытался использовать обычные инструменты mockito, чтобы издеваться над валидатором, но это тоже не сработало. Затем попытался установить валидатор в вызов mockMvc, но он не регистрирует аннотацию @Mock для валидатора. Наконец, столкнулся с этим вопросом. Но так как на самом контроллере нет поля validator, это тоже не сработает. Итак, как я могу исправить это, чтобы работать?

Validator:

public class TerminationValidator implements Validator {
    // JSR-303 Bean Validator utility which converts ConstraintViolations to Spring 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 [[email protected]]
    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)

Ответ 1

Вам следует избегать создания бизнес-объектов с new в приложении Spring. Вы всегда должны получать их из контекста приложения - это облегчит насмешку над ними в вашем тесте.

В вашем случае использования вы должны просто создать свой валидатор как bean (скажем 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" });
    }

    [...]
}

Таким образом, вы сможете просто ввести макет в свой тест.

Ответ 2

Ну, единственный способ, которым я знаю, справляться с этими ситуациями, не изменяя код приложения, используя PowerMock.

Он может управлять JVM и создавать mocks не только для статических методов, но также и при вызове оператора 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 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.