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
Mocking a Spring Validator when unit testing Controller
提问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 mockMvc
call, but that doesn't register any more than a @Mock
annotation for the validator. Finally ran into this question. But since there's no field validator
on 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.accesspublic 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 new
in 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 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 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 的情况下对其进行测试。