Mocking a Spring Validator when unit testing Controller

18,911

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.

Share:
18,911

Related videos on Youtube

t0mppa
Author by

t0mppa

As a web developer, have mostly worked with java (struts/spring/playframework), some experience also with ruby (rails/cuba), perl, clojure &amp; 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, 2022

Comments

  • t0mppa
    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 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)
    
  • t0mppa
    t0mppa about 9 years
    Would 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
    t0mppa about 9 years
    This 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
    jfcorugedo about 9 years
    Hi 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
    t0mppa about 9 years
    Well, 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
    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
    jfcorugedo about 9 years
    On 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
    Alex almost 3 years
    I'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?