Java 单元测试控制器时模拟Spring验证器
在为另一个项目创建的代码编写单元测试后期测试时,我遇到了这样一个问题:如何使用Java 单元测试控制器时模拟Spring验证器,java,unit-testing,spring-mvc,mockito,spring-mvc-test,Java,Unit Testing,Spring Mvc,Mockito,Spring Mvc Test,在为另一个项目创建的代码编写单元测试后期测试时,我遇到了这样一个问题:如何使用initBinder模拟绑定到控制器的验证器 通常我会考虑确保我的输入是有效的,并且在验证器中完成一些额外的调用,但是在这种情况下,ValueActer类与通过几个数据源进行检查耦合起来,测试变得非常混乱。耦合可以追溯到使用的一些旧的公共库,并且超出了我当前修复所有这些库的工作范围 起初,我尝试使用PowerMock和mocking静态方法模拟验证器的外部依赖关系,但最终在创建类时遇到了一个需要数据源的类,并且没有找到
initBinder
模拟绑定到控制器的验证器
通常我会考虑确保我的输入是有效的,并且在验证器中完成一些额外的调用,但是在这种情况下,ValueActer类与通过几个数据源进行检查耦合起来,测试变得非常混乱。耦合可以追溯到使用的一些旧的公共库,并且超出了我当前修复所有这些库的工作范围
起初,我尝试使用PowerMock和mocking静态方法模拟验证器的外部依赖关系,但最终在创建类时遇到了一个需要数据源的类,并且没有找到绕过该类的方法 然后我尝试使用普通的mockito工具来模拟验证器,但这也不起作用。然后尝试在mockMvc
调用中设置验证器,但这只注册了验证器的@Mock
注释。终于遇到了。但由于控制器本身没有字段验证程序
,因此也会失败。那么,我怎样才能解决这个问题呢
验证程序:
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)
测试类:
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)
例外情况:
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插入指令,并不仅为静态方法创建模拟,还可以在调用
new
操作符时创建模拟
看看这个例子:
如果要使用Mockito,必须使用PowerMockito而不是PowerMock:
阅读《如何模拟新对象的构造》
例如:
我的自定义控制器
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!!
}
}
我的自定义验证程序
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>
朱尼特
朱尼特
4.12
测试
org.powermock
powermock-module-junit4
1.6.2
测试
org.powermock
powermock api mockito
1.6.2
测试
org.mockito
莫基托磁芯
1.10.19
测试
正如您在我的测试中所看到的,我正在模拟验证器的行为,因此当控制器调用它时,它会抛出一个异常
然而,使用PowerMock通常表示设计不好。当您必须测试遗留应用程序时,通常必须使用它
如果您可以更改应用程序,那么最好更改代码,这样就可以在不插入JVM的情况下对其进行测试。如果您可以提供示例代码,那就太好了。在我的搜索过程中阅读了这两个页面,但没有找到使其工作的方法。这不是很像Spring,也没有解决最初的问题。另外,如果有可能的话,当你自己的帖子说为了避免使用PowerMock,最好更改应用程序时,你要求我更改解决方案中的实现。嗨,t0mppa,我将尝试改进我的回答:你是在编写单元测试还是集成测试?如果您正在编写单元测试,则不需要spring或任何其他容器。另一方面,如果您需要模拟
new
语句,那么编写单元测试就会遇到困难。当开发人员在编写解决方案时没有考虑以后必须编写的测试,这种情况经常发生。我的回答是:如果您不能更改应用程序代码,那么就使用PowerMock,但是请记住,一个好的类应该是可测试的,而不使用JVM工具依赖项注入应该使您的代码比传统javaee开发更少地依赖于容器。构成应用程序的POJO应该可以在JUnit或TestNG测试中进行测试,只需使用新操作符实例化对象,而不需要Spring或任何其他容器。您可以使用模拟对象(结合其他有价值的测试技术)来单独测试代码。如果您遵循Spring的架构建议,那么代码库的干净分层和组件化将有助于简化单元测试。
另一方面,您可以使用PowerMock来解决您所要求的问题,但使用PowerMock通常表示设计不好。我已经给出了一些代码示例来向您展示如何使用PowerMock,但是,正如您在我的答案末尾所看到的,我根本不喜欢PowerMock,因此如果您可以避免使用它,请使用变通方法。我在尝试让控制器方法测试调用其验证程序时遇到了问题。它是一个自定义的跨参数验证器,但它使用isValid()方法。如何将该类型的验证器传递给活页夹或mockMvc?