Mockito应该在Spring 4中与MockMvc的webAppContextSetup一起使用吗?

时间:2016-05-22 01:50:00

标签: spring-boot mockito spring-test spring-test-mvc

当我一起使用webAppContextSetup时,我很难让Mockito和MockMvc一起工作。我很好奇是不是因为我正在以一种他们从未想过的方式混合两者。

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

这是我正在运行的测试:

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

我期待当mockMvc执行该HTTP调用的模拟时,它会使用我在测试中定义的PersonController。但是当我调试时,它正在使用由SpringJunit4ClassRunner在测试启动时创建的PersonController。

我找到了两种方法让它发挥作用:

  1. 注入bean工厂,删除旧的personController单例,并添加我自己的。这很难看,而且我不是粉丝。
  2. 使用standaloneSetup而不是webAppContextSetup连接所有内容。我可以这样做,因为我不必触摸豆厂。
  3. 以下是我发现的一些不同的文章,有点涉及这个主题:

    思想?

4 个答案:

答案 0 :(得分:5)

您可能对Spring Boot 1.4中的new testing features感兴趣(特别是新的@MockBean注释)。这个sample显示了如何模拟服务并将其与控制器测试一起使用。

答案 1 :(得分:2)

出于某种原因,Mockito注释@Mock et @InjectMocks在这种情况下不起作用。

以下是我设法使其发挥作用的方式:

  • 使用您自己的测试上下文手动实例化personService bean
  • 让Mockito为此personService创建模拟。
  • 让Spring在控制器PersonController中注入这些模拟。

你应该有你的TestConfig:

@Configuration
public class ControllerTestConfig {

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

}

PersonControllerTest中,您不再需要personController,因为它由mockMvc通过perform方法管理。您也不需要执行initMocks(),因为您在Spring配置中手动初始化您的模拟。你应该有类似的东西:

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

答案 2 :(得分:0)

我有时会使用Mockito伪造使用@Primary@Profile注释的Spring bean。 I wrote a blog post about this technique.它还包含在GitHub上托管的完整工作示例的链接。

答案 3 :(得分:0)

为扩展florent的解决方案,我遇到了性能问题和可扩展性问题,为每个需要不同服务模拟集的控制器测试创建了单独的配置。因此,相反,我能够通过在我的测试旁边实现一个BeanPostProcessor来模拟我的应用程序的服务层,该模拟将所有@Service类替换为模拟:

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

我在带有@ActiveProfiles批注的特定测试中启用了这些模拟:

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

最后,注入的Mockito模拟被包装在AopProxy中,导致Mockito的expectverify调用失败。所以我为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;
  }