2

I'm having difficulties getting Mockito and MockMvc working together when I use the webAppContextSetup together. I'm curious if it's because I'm mixing the two in a way they were never intended.

Source: https://github.com/zgardner/spring-boot-intro/blob/master/src/test/java/com/zgardner/springBootIntro/controller/PersonControllerTest.java

Here is the test I'm running:

package com.zgardner.springBootIntro.controller;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.test.SpringApplicationConfiguration;
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;

import static java.lang.Math.toIntExact;
import static org.hamcrest.Matchers.is;
import static org.mockito.MockitoAnnotations.initMocks;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;

import com.zgardner.springBootIntro.Application;
import com.zgardner.springBootIntro.service.PersonService;
import com.zgardner.springBootIntro.model.PersonModel;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class PersonControllerTest {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext webApplicationContext;

    @Autowired
    private DefaultListableBeanFactory beanFactory;

    @Mock
    private PersonService personService;

    @InjectMocks
    private PersonController personController;

    @Before
    public void setup() {
        initMocks(this);

        // beanFactory.destroySingleton("personController");
        // beanFactory.registerSingleton("personController", personController);

        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }

    @Test
    public void getPersonById() throws Exception {
        Long id = 999L;
        String name = "Person name";

        when(personService.findById(id)).thenReturn(new PersonModel(id, name));

        mockMvc.perform(get("/person/getPersonById/" + id))
            .andDo(print())
            .andExpect(jsonPath("$.id", is(toIntExact(id))))
            .andExpect(jsonPath("$.name", is(name)));
    }
}

I was expecting that when mockMvc performed the mock of that HTTP call, it would use the PersonController I defined in my test. But when I debug through, it's using the PersonController which was created by the SpringJunit4ClassRunner on the test boot up.

I found two ways to get this to work:

  1. Inject the bean factory, remove the old personController singleton, and add my own. This is ugly, and I am not a fan.
  2. Wire everything up using the standaloneSetup instead of webAppContextSetup. I may do this instead as I don't have to touch the bean factory.

Here are some different articles I've found that somewhat touch on the topic:

Thoughts?

Community
  • 1
  • 1
Zach Gardner
  • 999
  • 2
  • 10
  • 13

4 Answers4

6

You might be interested in the new testing features coming in Spring Boot 1.4 (specifically the new @MockBean annotation). This sample shows how a service can be mocked and used with a controller test.

Phil Webb
  • 8,119
  • 1
  • 37
  • 37
3

For some reason the Mockito annotations @Mock et @InjectMocks won't work in this case.

Here's how I managed to make it work :

  • Instantiate the personService bean manually using your own Test context
  • make Mockito create a mock for this personService.
  • let Spring inject these mocks in the controller PersonController.

You should have your TestConfig :

@Configuration
public class ControllerTestConfig {

  @Bean
  PersonService personService() {
    return mock(PersonService.class);
  }

}

In your PersonControllerTest, you won't need the personController anymore, since it's managed by the mockMvc through the perform method. You also don't need to execute initMocks() because you initialize your mocks manually inside the Spring config. You should have something like :

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {Application.class, ControllerTestConfig.class})
@WebAppConfiguration
public class PersonControllerTest {

  private MockMvc mockMvc;

  @Autowired
  private WebApplicationContext webApplicationContext;

  @Autowired
  PersonService personService;

  @Before
  public void setup() {
    mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
  }

  @Test
  public void getPersonById() throws Exception {
    Long id = 999L;
    String name = "Person name";

    when(personService.findById(id)).thenReturn(new PersonModel(id, name));

    mockMvc.perform(get("/person/getPersonById/" + id))
        .andDo(print())
        .andExpect(jsonPath("$.id", is(toIntExact(id))))
        .andExpect(jsonPath("$.name", is(name)));
  }
}
Florent Dupont
  • 1,758
  • 18
  • 24
1

I sometimes use Mockito to fake Spring beans with usage of @Primary and @Profile annotations. I wrote a blog post about this technique. It also contains link to fully working example hosted on GitHub.

luboskrnac
  • 23,973
  • 10
  • 81
  • 92
1

To extend florent's solution, I encountered performance issues and extensibility issues creating separate configurations for every controller test which needed a different set of service mocks. So instead, I was able to mock out my application's service layer by implementing a BeanPostProcessor alongside my tests which replaces all @Service classes with mocks:

@Component
@Profile("mockService")
public class AbcServiceMocker implements BeanPostProcessor {

  private static final String ABC_PACKAGE = "com.mycompany.abc";

  @Override
  public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
    if (StringUtils.startsWith(bean.getClass().getPackage().getName(), ABC_PACKAGE)) {
      if (AnnotationUtils.isAnnotationDeclaredLocally(Service.class, bean.getClass())
          || AnnotationUtils.isAnnotationInherited(Service.class, bean.getClass())) {
        return mock(bean.getClass());
      }
    }
    return bean;
  }

  @Override
  public Object postProcessAfterInitialization(Object bean, String name) throws BeansException {
    return bean;
  }
}

I enabled these mocks in specific tests with an @ActiveProfiles annotation:

@WebAppConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:/WEB-INF/application-context.xml"})
@ActiveProfiles("mockService")
public class AbcControllerTest {

  private MockMvc mvc;

  @Before
  public final void testBaseSetup() {
    mvc = MockMvcBuilders.webAppContextSetup(context).build();
  }

Lastly, the injected Mockito mocks were wrapped in an AopProxy causing Mockito's expect and verify calls to fail. So I wrote a utility method to unwrap them:

  @SuppressWarnings("unchecked")
  protected <T> T mockBean(Class<T> requiredType) {
    T s = context.getBean(requiredType);
    if (AopUtils.isAopProxy(s) && s instanceof Advised) {
      TargetSource targetSource = ((Advised) s).getTargetSource();
      try {
        return (T) targetSource.getTarget();
      } catch (Exception e) {
        throw new RuntimeException("Error resolving target", e);
      }
    }
    Mockito.reset(s);
    return s;
  }
piepera
  • 2,033
  • 1
  • 20
  • 21