java 在单元测试控制器时模拟 Spring 验证器

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/29538695/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-02 15:27:43  来源:igfitidea点击:

Mocking a Spring Validator when unit testing Controller

javaunit-testingspring-mvcmockitospring-mvc-test

提问by t0mppa

While writing unit tests postmortem to code that another project created, I came across this issue of how to mock a validator that is bound to the controller with initBinder?

在编写单元测试事后编写另一个项目创建的代码时,我遇到了这个问题,即如何模拟绑定到控制器的验证器initBinder

Normally I would just consider making sure my inputs are valid and be done with a few extra calls in the validator, but in this case the validator class is coupled with doing checks through a few data sources and it all becomes quite a mess to test. Coupling dates back to some old common libraries used and is outside the scope of my current work to fix all of them.

通常我会考虑确保我的输入是有效的,并在验证器中通过一些额外的调用来完成,但在这种情况下,验证器类与通过一些数据源进行检查相结合,这一切都变得一团糟,无法测试。耦合可以追溯到一些旧的常用库,这超出了我当前修复所有这些库的工作范围。

At first I tried to just mock out the external dependencies of the validator using PowerMock and mocking static methods, but eventually ran into a class that requires a data source when the class is created and didn't find a way around that one.

起初,我尝试使用 PowerMock 和模拟静态方法来模拟验证器的外部依赖关系,但最终遇到一个在创建类时需要数据源的类,并且没有找到解决方法。

Then I tried to just use normal mockito tools to mock out the validator, but that didn't work either. Then tried to set the validator in the mockMvccall, but that doesn't register any more than a @Mockannotation for the validator. Finally ran into this question. But since there's no field validatoron the controller itself, this fails too. So, how can I fix this to work?

然后我尝试只使用普通的 mockito 工具来模拟验证器,但这也不起作用。然后尝试在mockMvc调用中设置验证器,但这只是@Mock验证器的注释。终于碰到这个问题了。但是由于validator控制器本身没有字段,这也失败了。那么,我该如何解决这个问题呢?

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

        [...]
    }

    [...]
}

Controller:

控制器:

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

    [...]
}

Test class:

测试类:

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

    [...]
}

Exception:

例外:

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.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.run(ParentRunner.java:238)
    at org.junit.runners.ParentRunner.schedule(ParentRunner.java:63)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
    at org.junit.runners.ParentRunner.access
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" });
    }

    [...]
}
0(ParentRunner.java:53) at org.junit.runners.ParentRunner.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)

采纳答案by Serge Ballesta

You should avoid creating business objects with newin a Spring application. You should always get them from the application context - it will ease mocking them in your test.

您应该避免new在 Spring 应用程序中创建业务对象。您应该始终从应用程序上下文中获取它们 - 它会在您的测试中轻松模拟它们。

In your use case, you should simply create your validator as a bean (say defaultTerminationValidator) and inject it in your controller :

在您的用例中,您应该简单地将验证器创建为 bean(例如defaultTerminationValidator)并将其注入您的控制器:

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

That way, you will be able to simply inject a mock in your test.

这样,您就可以简单地在测试中注入一个模拟。

回答by jfcorugedo

Well, the only way I know to deal with this situations, without changing your application code, using PowerMock.

好吧,我知道处理这种情况的唯一方法是使用 PowerMock,而无需更改您的应用程序代码。

It can instrument the JVM and creates mocks not only for static methods but also when you call newoperator.

它可以检测 JVM 并不仅为静态方法创建模拟,还可以在您调用new操作符时创建模拟。

Take a look at this example:

看看这个例子:

https://code.google.com/p/powermock/wiki/MockConstructor

https://code.google.com/p/powermock/wiki/MockConstructor

If you want to use Mockito, you have to use PowerMockito instead of PowerMock:

如果要使用 Mockito,则必须使用 PowerMockito 而不是 PowerMock:

https://code.google.com/p/powermock/wiki/MockitoUsage13

https://code.google.com/p/powermock/wiki/MockitoUsage13

Read the section How to mock construction of new objects

阅读部分 How to mock construction of new objects

For instance:

例如:

My custom controller

我的自定义控制器

public class CoolValidator {

    public void validate(String input) throws InvalidParameterException {
        //Do some validation. This code will be mocked by PowerMock!!
    }
}

My custom validator

我的自定义验证器

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

    }
}

My custom test using PowerMockito

我使用 PowerMockito 的自定义测试

<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>

Maven dependencies

Maven 依赖项

##代码##

As you can see in my test, I'm mocking the validator behaviour, so it throws an exception when the controller invokes it.

正如您在我的测试中看到的那样,我在模拟验证器行为,因此当控制器调用它时它会抛出异常。

However, the use of PowerMock usually denotes a bad design. It must be used typically when you have to test a legacy application.

但是,使用 PowerMock 通常表示设计不佳。当您必须测试遗留应用程序时,通常必须使用它。

If you can change the application, better change the code so it can be tested without instrumenting the JVM.

如果您可以更改应用程序,最好更改代码,以便可以在不检测 JVM 的情况下对其进行测试。