单元测试控制器时模拟Spring Validator



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

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

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

然后我试着只使用普通的mockito工具来模拟验证器,但这也不起作用。然后尝试在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)

您应该避免在Spring应用程序中使用new创建业务对象。您应该始终从应用程序上下文中获取它们——这将简化在测试中对它们的嘲讽。

在您的用例中,您应该简单地将验证器创建为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" });
    }
    [...]
}

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

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

它可以检测JVM,并不仅为静态方法创建mock,而且在调用new运算符时也可以创建mock。

看看这个例子:

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的情况下对其进行测试。

相关内容

  • 没有找到相关文章

最新更新