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
?
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.
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?
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. 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 :
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.
It can instrument the JVM and creates mocks not only for static methods but also when you call new
operator.
Take a look at this example:
https://code.google.com/p/powermock/wiki/MockConstructor
If you want to use Mockito, you have to use PowerMockito instead of PowerMock:
https://code.google.com/p/powermock/wiki/MockitoUsage13
Read the section 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
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
<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. 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With