Java – simulate a spring validator when unit testing the controller
When coding the unit test postmortem into the code created by another project, I encountered the problem of how to use initbinder to simulate the validator bound to the controller?
I usually consider making sure my input is valid and making some additional calls in the validator, but in this case, the validator class is combined with checking through several data sources, and the test becomes quite troublesome Coupling can be traced back to some old common libraries used, and it is not in my current scope to fix them
At first, I tried to use powermock and mock static methods to mock the external dependencies of the verifier, but I finally encountered a class. When the class was created, a data source was required, and no method was found
Then I tried to use the normal mockito tool to laugh at the validator, but I didn't Then try to set the verifier in the mockmvc call, but do not register the @ mock annotation for the verifier Finally, I ran to this question However, because the controller itself has no field verifier, it also failed So, how can I solve this problem?
Verifier:
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)); } [...] }
Exceptions:
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)
Solution
You should avoid creating new business objects in spring applications You should always get them from the context of your application - it will easily mock them in your tests
In your use case, you should simply create a bean validator (such as defaultterminationvalidator) and inject it into your controller:
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[**]","bic" }); } [...] }
This allows you to simply inject simulations into your tests