Mocking a Spring Validator when unit testing Controller
Solution 1
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.
Solution 2
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.
Related videos on Youtube
t0mppa
As a web developer, have mostly worked with java (struts/spring/playframework), some experience also with ruby (rails/cuba), perl, clojure & nodejs. SO has been a big help to me and hoping to continue learning and share something back to the community.
Updated on June 04, 2022Comments
-
t0mppa almost 2 years
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 fieldvalidator
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)
-
t0mppa about 9 yearsWould be nice, if you could provide sample code to go along with this. Read both of those pages during my searches, but didn't find a way to get it to work.
-
t0mppa about 9 yearsThis isn't very Spring like, nor does it fix the original problem. Plus you're asking me to change the implementation in your solution, when your own post says that it's better to change the application in order to avoid using PowerMock, if that's a possibility.
-
jfcorugedo about 9 yearsHi t0mppa, I'll try to improve my answer: Are you coding an unit test or an integration test? If you're coding unit tests, then you don't need spring or any other container. On the other hand, if you need to mock
new
statements, you'll have trouble coding unit tests. It often happens when the developer codes the solution without thinking in the test that must be coded later. My answer says: if you can't change the application code, then use PowerMock, but take in mind that a good class should be testable without the use of JVM instrumentation -
t0mppa about 9 yearsWell, if one defines unit & integration testing by whether they need Spring or not, we're talking about integration testing then, even though I was just using Spring MVC Test framework to test what happens within the controller and wanted to mock out the other classes. My point mainly was that if I change application code like you suggest in order to make PowerMock work, then I'm actually going against your own advice. Thus the answer is a paradox concerning the code.
-
jfcorugedo about 9 years
Dependency Injection should make your code less dependent on the container than it would be with traditional Java EE development. The POJOs that make up your application should be testable in JUnit or TestNG tests, with objects simply instantiated using the new operator, without Spring or any other container. You can use mock objects (in conjunction with other valuable testing techniques) to test your code in isolation. If you follow the architecture recommendations for Spring, the resulting clean layering and componentization of your codebase will facilitate easier unit testing.
-
jfcorugedo about 9 yearsOn the other hand, you can use PowerMock to solve the problem you're asking for, but the use of PowerMock usually denotes a bad design. I've put some code examples to show you how to use PowerMock, but, as you can read at the end of my answer, I don't like PowerMock at all, so if you can avoid using it, use a workaround.
-
Alex almost 3 yearsI'm having issues trying to make a controller method test call its validator. It is a custom cross-parameter validator but it uses the isValid() method. How would you pass a validator of that type to a binder or mockMvc?