Unable to mock Service class in Spring MVC Controller tests

后端 未结 7 770
渐次进展
渐次进展 2020-12-02 06:44

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

相关标签:
7条回答
  • 2020-12-02 06:56

    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.

    0 讨论(0)
  • 2020-12-02 07:01

    There is another solution with latest spring release using @WebMvcTest. Example below.

    @RunWith(SpringRunner.class)
    @WebMvcTest(CategoryAPI.class)
    public class CategoryAPITest {
    
    @Autowired
    private MockMvc mvc;
    
    @MockBean
    CategoryAPIService categoryAPIService;
    
    @SpyBean
    Utility utility;
    
    PcmResponseBean responseBean;
    
    @Before
    public void before() {
        PcmResponseBean responseBean = new PcmResponseBean("123", "200", null, null);
        BDDMockito.given(categoryAPIService.saveCategory(anyString())).willReturn(responseBean);
    }
    
    @Test
    public void saveCategoryTest() throws Exception {
        String category = "{}";
        mvc.perform(post("/api/category/").content(category).contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk()).andExpect(jsonPath("messageId", Matchers.is("123")))
                .andExpect(jsonPath("status", Matchers.is("200")));
      }
    
    }
    

    Here we are loading only the CategoryAPI class which is a Spring rest controller class and rest all are mock. Spring has own version of annotation like @MockBean and and @SpyBean similar to mockito @Mock and @Spy.

    0 讨论(0)
  • 2020-12-02 07:01

    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.

    0 讨论(0)
  • 2020-12-02 07:02

    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;".

    0 讨论(0)
  • 2020-12-02 07:07

    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;
    
    0 讨论(0)
  • 2020-12-02 07:18

    If on SpringBoot use Springs own @MockBean. - as the docks say:

    Any existing single bean of the same type defined in the context will be replaced by the mock. If no existing bean is defined a new one will be added.

     @RunWith(SpringRunner.class)
     public class ExampleTests {
    
         @MockBean
         private ExampleService service;
    
    0 讨论(0)
提交回复
热议问题