I have a Spring 3.2 MVC application and am using the Spring MVC test framework to test GET and POST requests on the actions of my controllers. I am using Mockito to mock the Services but am finding that the mocks are being ignored and that my actual Service layer is being used (and, as a consequence, the database is being hit).
The code in my Controller test:
package name.hines.steven.medical_claims_tracker.controllers; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; import name.hines.steven.medical_claims_tracker.domain.Policy; import name.hines.steven.medical_claims_tracker.services.PolicyService; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration({ "classpath:/applicationContext.xml", "classpath:/tests_persistence-applicationContext.xml" }) public class PolicyControllerTest { @Mock PolicyService service; @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); // this must be called for the @Mock annotations above to be processed. MockitoAnnotations.initMocks(this); } @Test public void createOrUpdateFailsWhenInvalidDataPostedAndSendsUserBackToForm() throws Exception { // Post no parameters in this request to force errors mockMvc.perform(post("/policies/persist")).andExpect(status().isOk()) .andExpect(model().attributeHasErrors("policy")) .andExpect(view().name("createOrUpdatePolicy")); } @Test public void createOrUpdateSuccessful() throws Exception { // Mock the service method to force a known response when(service.save(isA(Policy.class))).thenReturn(new Policy()); mockMvc.perform( post("/policies/persist").param("companyName", "Company Name") .param("name", "Name").param("effectiveDate", "2001-01-01")) .andExpect(status().isMovedTemporarily()).andExpect(model().hasNoErrors()) .andExpect(redirectedUrl("list")); } }
You'll notice I have two context configuration files; this is a hack because if I'm unable to stop the controller test hitting the actual service layer then that service layer might as well have its repositories pointing at the test database. I'm not at a point where I can't get away with this hack any longer and need to be able to mock out my service layer properly.
Why is the when(service.save(isA(Policy.class))).thenReturn(new Policy());
not kicking in and mocking out the save method in the PolicyService? Am I missing some mockito configuration somewhere? Is there something I need to put in the Spring configuration? My reasearch so far has been limited to Googling "spring mvc test mockito not working" but that has not given me much to go on.
Thanks.
You were right @tom-verelst, I was referring to the PolicyService service;
line in my test so the service inside the MockMvc
will of course have been injected by Spring.
I did a bit of research and found a blog post which did a good job of explaining what @InjectMocks
is used for.
I then tried annotating private MockMvc mockMvc
with @InjectMocks
and still got the same problem (i.e. the service inside the MockMvc
was not mocked as I was expecting it to be). I have added the stack trace at the point during debugging where the save method on the PolicyServiceImpl
is called (as opposed to the desired call to the save method in the mocked service).
Thread [main] (Suspended (breakpoint at line 29 in DomainEntityServiceImpl) PolicyServiceImpl(DomainEntityServiceImpl<T>).save(T) line: 29 NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method] NativeMethodAccessorImpl.invoke(Object, Object[]) line: 39 DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 25 Method.invoke(Object, Object...) line: 597 AopUtils.invokeJoinpointUsingReflection(Object, Method, Object[]) line: 317 ReflectiveMethodInvocation.invokeJoinpoint() line: 183 ReflectiveMethodInvocation.proceed() line: 150 TransactionInterceptor$1.proceedWithInvocation() line: 96 TransactionInterceptor(TransactionAspectSupport).invokeWithinTransaction(Method, Class, TransactionAspectSupport$InvocationCallback) line: 260 TransactionInterceptor.invoke(MethodInvocation) line: 94 ReflectiveMethodInvocation.proceed() line: 172 JdkDynamicAopProxy.invoke(Object, Method, Object[]) line: 204 $Proxy44.save(DomainEntity) line: not available PolicyController.createOrUpdate(Policy, BindingResult) line: 64 NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method] NativeMethodAccessorImpl.invoke(Object, Object[]) line: 39 DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 25 Method.invoke(Object, Object...) line: 597 ServletInvocableHandlerMethod(InvocableHandlerMethod).invoke(Object...) line: 219 ServletInvocableHandlerMethod(InvocableHandlerMethod).invokeForRequest(NativeWebRequest, ModelAndViewContainer, Object...) line: 132 ServletInvocableHandlerMethod.invokeAndHandle(ServletWebRequest, ModelAndViewContainer, Object...) line: 104 RequestMappingHandlerAdapter.invokeHandleMethod(HttpServletRequest, HttpServletResponse, HandlerMethod) line: 746 RequestMappingHandlerAdapter.handleInternal(HttpServletRequest, HttpServletResponse, HandlerMethod) line: 687 RequestMappingHandlerAdapter(AbstractHandlerMethodAdapter).handle(HttpServletRequest, HttpServletResponse, Object) line: 80 TestDispatcherServlet(DispatcherServlet).doDispatch(HttpServletRequest, HttpServletResponse) line: 925 TestDispatcherServlet(DispatcherServlet).doService(HttpServletRequest, HttpServletResponse) line: 856 TestDispatcherServlet(FrameworkServlet).processRequest(HttpServletRequest, HttpServletResponse) line: 915 TestDispatcherServlet(FrameworkServlet).doPost(HttpServletRequest, HttpServletResponse) line: 822 TestDispatcherServlet(HttpServlet).service(HttpServletRequest, HttpServletResponse) line: 727 TestDispatcherServlet(FrameworkServlet).service(HttpServletRequest, HttpServletResponse) line: 796 TestDispatcherServlet.service(HttpServletRequest, HttpServletResponse) line: 66 TestDispatcherServlet(HttpServlet).service(ServletRequest, ServletResponse) line: 820 MockFilterChain$ServletFilterProxy.doFilter(ServletRequest, ServletResponse, FilterChain) line: 168 MockFilterChain.doFilter(ServletRequest, ServletResponse) line: 136 MockMvc.perform(RequestBuilder) line: 134 PolicyControllerTest.createOrUpdateSuccessful() line: 67 NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method] NativeMethodAccessorImpl.invoke(Object, Object[]) line: 39 DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 25 Method.invoke(Object, Object...) line: 597 FrameworkMethod$1.runReflectiveCall() line: 44 FrameworkMethod$1(ReflectiveCallable).run() line: 15 FrameworkMethod.invokeExplosively(Object, Object...) line: 41 InvokeMethod.evaluate() line: 20 RunBefores.evaluate() line: 28 RunBeforeTestMethodCallbacks.evaluate() line: 74 RunAfterTestMethodCallbacks.evaluate() line: 83 SpringRepeat.evaluate() line: 72 SpringJUnit4ClassRunner.runChild(FrameworkMethod, RunNotifier) line: 231 SpringJUnit4ClassRunner.runChild(Object, RunNotifier) line: 88 ParentRunner$3.run() line: 193 ParentRunner$1.schedule(Runnable) line: 52 SpringJUnit4ClassRunner(ParentRunner<T>).runChildren(RunNotifier) line: 191 ParentRunner<T>.access$000(ParentRunner, RunNotifier) line: 42 ParentRunner$2.evaluate() line: 184 RunBeforeTestClassCallbacks.evaluate() line: 61 RunAfterTestClassCallbacks.evaluate() line: 71 SpringJUnit4ClassRunner(ParentRunner<T>).run(RunNotifier) line: 236 SpringJUnit4ClassRunner.run(RunNotifier) line: 174 JUnit4TestMethodReference(JUnit4TestReference).run(TestExecution) line: 50 TestExecution.run(ITestReference[]) line: 38 RemoteTestRunner.runTests(String[], String, TestExecution) line: 467 RemoteTestRunner.runTests(TestExecution) line: 683 RemoteTestRunner.run() line: 390 RemoteTestRunner.main(String[]) line: 197
More research (Mockito Injecting Null values into a Spring bean when using @Mock?) suggested applying the @InjectMocks
to a PolicyController
member variable within the test, but as pointed out in one of the answers in the first link, this does nothing because Spring doesn't know anything about it.
Thanks to @J Andy's line of thought, I realised that I had been heading down the wrong path on this. In Update 1 I was trying to inject the mock service into the MockMvc
but after taking a step back I realised that it's not the MockMvc
that was under test, it was the PolicyController
I wanted to test.
To give a bit of background, I wanted to avoid a traditional unit test of the @Controllers in my Spring MVC application because I wanted to test things that are only provided by running the controllers within Spring itself (e.g. RESTful calls to controller actions). This can be achieved by using the Spring MVC Test framework which allows you to run your tests within Spring.
You'll see from the code in my initial question that I was running the Spring MVC tests in a WebApplicationContext
(i.e. this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
) whereas what I should have been doing was running standalone. Running standalone allows me to directly inject the controller I want to test and, therefore, have control over how the service is injected into the controller (i.e. force a mock service to be used).
This is easier explained in code. So for the following controller:
import javax.validation.Valid; import name.hines.steven.medical_claims_tracker.domain.Benefit; import name.hines.steven.medical_claims_tracker.domain.Policy; import name.hines.steven.medical_claims_tracker.services.DomainEntityService; import name.hines.steven.medical_claims_tracker.services.PolicyService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.ModelAndView; @Controller @RequestMapping("/policies") public class PolicyController extends DomainEntityController<Policy> { @Autowired private PolicyService service; @RequestMapping(value = "persist", method = RequestMethod.POST) public String createOrUpdate(@Valid @ModelAttribute("policy") Policy policy, BindingResult result) { if (result.hasErrors()) { return "createOrUpdatePolicyForm"; } service.save(policy); return "redirect:list"; } }
I now have the following test class in which the service is successfully mocked out and my test database is no longer hit:
package name.hines.steven.medical_claims_tracker.controllers; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; import name.hines.steven.medical_claims_tracker.domain.Policy; import name.hines.steven.medical_claims_tracker.services.PolicyService; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration({ "classpath:/applicationContext.xml" }) public class PolicyControllerTest { @Mock PolicyService policyService; @InjectMocks PolicyController controllerUnderTest; private MockMvc mockMvc; @Before public void setup() { // this must be called for the @Mock annotations above to be processed // and for the mock service to be injected into the controller under // test. MockitoAnnotations.initMocks(this); this.mockMvc = MockMvcBuilders.standaloneSetup(controllerUnderTest).build(); } @Test public void createOrUpdateFailsWhenInvalidDataPostedAndSendsUserBackToForm() throws Exception { // POST no data to the form (i.e. an invalid POST) mockMvc.perform(post("/policies/persist")).andExpect(status().isOk()) .andExpect(model().attributeHasErrors("policy")) .andExpect(view().name("createOrUpdatePolicy")); } @Test public void createOrUpdateSuccessful() throws Exception { when(policyService.save(isA(Policy.class))).thenReturn(new Policy()); mockMvc.perform( post("/policies/persist").param("companyName", "Company Name") .param("name", "Name").param("effectiveDate", "2001-01-01")) .andExpect(status().isMovedTemporarily()).andExpect(model().hasNoErrors()) .andExpect(redirectedUrl("list")); } }
I'm still very much learning when it comes to Spring so any comments that will improve my explanation would be welcomed. This blog post was helpful to me in coming up with this solution.
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