当我一起使用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调用的mock时,会使用我在测试中定义的PersonController。但当我调试完成时,它使用的是由SpringJunit4ClassRunner在测试启动时创建的PersonController。
我找到了两种方法来实现这一点:
- 注入bean工厂,删除旧的personController singleton,并添加我自己的。这太难看了,我不是粉丝
- 使用standaloneSetup而不是webAppContextSetup连接所有内容。我可以这样做,因为我不必碰豆厂
以下是我发现的一些不同的文章,它们在某种程度上触及了这个主题:
- Spring教程-构建REST服务这只是在进行集成测试之前自动连接repo以清除数据
- 使用SpringMVC测试框架和Mockito来测试控制器这使用了Mockito和webAppContextSetup,但这是在Spring3中。(我使用的是Spring Boot)
- 无法在Spring MVC Controller测试中模拟Service类这使用了standaloneSetup,它在我的情况下也能工作
想法?
您可能对Spring Boot 1.4中的新测试功能(特别是新的@MockBean
注释)感兴趣。此示例显示了如何模拟服务并将其与控制器测试一起使用。
@Mock
和@InjectMocks
在这种情况下不起作用。
以下是我如何使其发挥作用:
- 使用自己的测试上下文手动实例化
personService
bean - 让Mockito为这个
personService
创建一个mock - 让Spring在控制器
PersonController
中注入这些mock
您应该有您的TestConfig:
@Configuration
public class ControllerTestConfig {
@Bean
PersonService personService() {
return mock(PersonService.class);
}
}
在PersonControllerTest
中,您将不再需要personController
,因为它是由mockMvc
通过perform
方法管理的。您也不需要执行initMocks()
,因为您可以在Spring配置中手动初始化mock。你应该有这样的东西:
@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)));
}
}
我有时会使用Mockito通过使用@Primary
和@Profile
注释来伪造Springbean。我写了一篇关于这项技术的博客文章。它还包含GitHub上托管的完整工作示例的链接。
为了扩展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 mock被封装在AopProxy中,导致Mockito的expect
和verify
调用失败。所以我写了一个实用的方法来打开它们:
@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;
}