Unable to mock Service class in Spring MVC Controller tests

给你一囗甜甜゛ 提交于 2019-11-28 03:08:45

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.

This section, 11.3.6 Spring MVC Test Framework, in Spring document 11. Testing talks about it, but it is not clear in someway.

Let's continue with the example in the document for explanation. The sample testing class looks as follow

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration("test-servlet-context.xml")
public class AccountTests {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Autowired
    private AccountService accountService;

    // ...

}

Suppose you have org.example.AppController as the controller. In the test-servlet-context.xml, you will need to have

<bean class="org.example.AppController">
    <property name="accountService" ref="accountService" />
</bean>

<bean id="accountService" class="org.mockito.Mockito" factory-method="mock">
    <constructor-arg value="org.example.AccountService"/>
</bean>

The document is missing the wiring part for the controller. And you will need change to setter injection for accountService if you are using field injection. Also, be noted that the value(org.example.AccountService here) for constructor-arg is an interface, not a class.

In the setup method in AccountTests, you will have

@Before
public void setup() {
    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();

    // You may stub with return values here
    when(accountService.findById(1)).thenReturn(...);
}

The test method may look like

@Test
public void testAccountId(){
    this.mockMvc.perform(...)
    .andDo(print())
    .andExpect(...);  
}

andDo(print()) comes handy, do "import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;".

Swarit Agarwal

I would prefer standalone service of Mockmvc

Mentioned work for me

public class AccessControllerTest {

    private MockMvc mockMvc;

    @Mock
    private AccessControlService accessControlService;

    @InjectMocks
    private AccessController accessController;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        this.mockMvc =  MockMvcBuilders.standaloneSetup(accessController).build();
    }

    @Test
    public void validAccessControlRequest() throws Exception {
        Bundle bundle = new Bundle();
        bundle.setAuthorized(false);
        Mockito.when(accessControlService.retrievePatient(any(String.class)))
         .thenReturn(bundle);

        mockMvc.perform(get("/access/user?user=3")).andExpect(status().isOk());
}

This is probably an issue with Spring and Mockito attempting to both inject the beans. One way I can think of avoiding these issues is to use Spring ReflectionTestUtils to manually inject the service mock.

In this case your setup() method would look something like this

@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);

    // TODO: Make sure to set the field name in UUT correctly
    ReflectionTestUtils.setField( mockMvc, "service", service );
}

P.S. Your naming convention are bit off IMHO and I'm assuming that mockMvc is the class you're trying to test (UUT). I'd use following names instead

@Mock PolicyService mockPolicyService;
@InjectMocks Mvc mvc;
Tom Verelst

You're creating a mock for PolicyService, but you're not injecting it into your MockMvc as far as I can tell. This means that the PolicyService defined in your Spring configuration will be called instead of your mock.

Either inject the mock of the PolicyService into your MockMvc by setting it, or take a look at Springockito for injecting mocks.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!