简体   繁体   English

在单元测试Controller时模拟一个Spring Validator

[英]Mocking a Spring Validator when unit testing Controller

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. 然后我尝试使用普通的mockito工具模拟验证器,但这也不起作用。 Then tried to set the validator in the mockMvc call, but that doesn't register any more than a @Mock annotation for the validator. 然后尝试在mockMvc调用中设置验证器,但是它不会为验证器注册任何@Mock注释。 Finally ran into this question . 最后遇到了这个问题 But since there's no field validator on the controller itself, this fails too. 但由于控制器本身没有字段validator ,因此也失败了。 So, how can I fix this to work? 那么,我该如何解决这个问题呢?

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

You should avoid creating business objects with new in a Spring application. 您应该避免在Spring应用程序中使用new创建业务对象。 You should always get them from the application context - it will ease mocking them in your test. 您应该始终从应用程序上下文中获取它们 - 它将在测试中轻松模拟它们。

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

    [...]
}

That way, you will be able to simply inject a mock in your test. 这样,您就可以在测试中简单地注入模拟。

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 new operator. 它可以检测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 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();
   }
}

My custom validator 我的自定义验证器

public class CoolValidator {

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

My custom test using PowerMockito 我使用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 dependencies 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>

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. 但是,PowerMock的使用通常表示糟糕的设计。 It must be used typically when you have to test a legacy application. 当您必须测试遗留应用程序时,通常必须使用它。

If you can change the application, better change the code so it can be tested without instrumenting the JVM. 如果您可以更改应用程序,则可以更好地更改代码,以便在不检测JVM的情况下对其进行测试。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 服务层测试:使用dbUnit时是否仍然是单元测试而不是模拟本机Spring域层? - Service layer testing: Is it still unit testing when using dbUnit and not mocking a native Spring domain layer? Spring 3.2.4中的PowerMockito模拟静态测试控制器 - PowerMockito mocking static testing controller in spring 3.2.4 模拟弹簧控制器用于单元测试 - Mock Spring Controller for unit testing 单元测试Spring RESTful控制器时“内容类型未设置” - “Content type not set” when Unit Testing Spring RESTful controller 在对 Spring REST 控制器进行单元测试时注入 @AuthenticationPrincipal - Inject @AuthenticationPrincipal when unit testing a Spring REST controller Mocking 一个 OpenFeign 客户端,用于 spring 库中的单元测试,而不是用于 spring 引导应用程序 - Mocking an OpenFeign client for Unit Testing in a spring library and NOT for a spring boot application 在 SpringBoot API 中测试时 controller 上出现 NullPointerException - Mocking - NullPointerException on controller when testing in SpringBoot API - Mocking 嘲笑单身人士进行单元测试 - Mocking a Singleton for Unit Testing 努力进行单元测试和模拟 - Struggling with unit testing and mocking 测试 Spring bean 时模拟配置属性 - Mocking a configuration property when testing a Spring bean
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM