Appearance
Spring Boot Test
本文介绍Spring Boot中对于测试的相关支持与特性。
1. @SprinBootTest注解
1.1 基础使用
@SpringBootTest注解用在测试类上,用于启动Spring Boot容器,例如:
java
@SpringBootTest
public class Demo {
@Test
public void test01() {}
}所谓启动Spring Boot容器,如下:
javaSpringApplication.run(SpringBootDemoApplication.class, args);只要启动的是Spring Boot容器,那么外部配置文件解析、日志等其他Spring Boot特性都默认支持了。
该注解的基础属性如下:
classes:用于指定加载容器的组件类,如果没有指定,那么Spring Boot Test会寻找测试类中的@Configuration类,如果@Configuration类也没有,那么Spring Boot Test会从测试类所在包,向上查找,直到查找到某个标注了@SpringBootApplication或@SpringBootConfiguration的类。这就是Spring Boot Test对Spring Test的增强,它不要求一定指定容器组件类,而是会自己去寻找配置类,如果代码结构组织良好,那么Spring Boot Test会找到我们的主类(
@SpringBootApplication)作为容器启动类。java@SpringBootApplication public class SpringBootDemoApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoApplication.class, args); } }value或properties:字符串数组,以key=value格式定义测试环境变量;args:@SpringBootTest启动的是Spring Boot容器,归根结底,就是运行以下代码:javaSpringApplication application = new SpringApplication(); application.run(args);这里的
args参数就是传入run()方法的;
1.2 基本原理
在Spring Test中,我们了解到ContextLoader是用来加载容器的,在Spring Boot Test中,实现了自己的容器加载器SpringBootContextLoader,其中的方法loadContext()就是用来加载容器的,部分源码如下:
java
private ApplicationContext loadContext(MergedContextConfiguration mergedConfig, Mode mode,
ApplicationContextInitializer<ConfigurableApplicationContext> initializer, RuntimeHints runtimeHints)
throws Exception {
assertHasClassesOrLocations(mergedConfig);
SpringBootTestAnnotation annotation = SpringBootTestAnnotation.get(mergedConfig);
String[] args = annotation.getArgs();
SpringApplication application = getSpringApplication();
configure(mergedConfig, application);
ContextLoaderHook hook = new ContextLoaderHook(mode, initializer, ALREADY_CONFIGURED);
return hook.run(() -> application.run(args));
}入参只需要关心MergedContextConfiguration mergedConfig,这是容器的综合配置,也就是说它决定了容器有哪些组件、环境变量等,它也是容器缓存的唯一索引。Spring 判断两个测试类是否可以共用同一个容器,就是通过它来决定的。
在MergedContextConfiguration中有以下关键属性:
| 属性 | 来源 | 作用 |
|---|---|---|
testClass | 测试类本身 | 确定主入口。 |
locations / classes | @ContextConfiguration | 确定哪些 XML 或配置类要被加载。 |
activeProfiles | @ActiveProfiles | 确定当前是什么环境。 |
propertySourceProperties | @TestPropertySource | 那些写在注解里的 foo=bar 键值对。 |
contextCustomizers | 各种扩展插件 | 关键点:@MockBean 的定义就是存在这里的。 |
下面来分析loadContext方法源码:
第四行:
assertHasClassesOrLocations(mergedConfig);,用来判断容器组件定义是否存在,如果不存在,则直接抛异常:javaprivate void assertHasClassesOrLocations(MergedContextConfiguration mergedConfig) { boolean hasClasses = !ObjectUtils.isEmpty(mergedConfig.getClasses()); boolean hasLocations = !ObjectUtils.isEmpty(mergedConfig.getLocations()); Assert.state(hasClasses || hasLocations, () -> "No configuration classes or locations found in @SpringApplicationConfiguration. " + "For default configuration detection to work you need Spring 4.0.3 or better (found " + SpringVersion.getVersion() + ")."); }mergedConfig.getClasses()获取容器组件类;关于如何获取组件类,在如下位置:
org.springframework.boot.test.context.SpringBootTestContextBootstrapper#getOrFindConfigurationClasses
javaprotected Class<?>[] getOrFindConfigurationClasses(MergedContextConfiguration mergedConfig) { Class<?>[] classes = mergedConfig.getClasses(); if (containsNonTestComponent(classes) || mergedConfig.hasLocations()) { return classes; } Class<?> found = findConfigurationClass(mergedConfig.getTestClass()); logger.info("Found @SpringBootConfiguration " + found.getName() + " for test " + mergedConfig.getTestClass()); return merge(found, classes); }mergedConfig.getLocations()获取容器组件定义XML路径;如果两者都不存在,则使用Assert抛异常。
第5-6行,获取
@SpringBootTest注解中的args属性值;第8行,创建
SpringApplication对象:javaSpringApplication application = getSpringApplication(); protected SpringApplication getSpringApplication() { return new SpringApplication(); }第9行,配置容器,这是关键代码:
javaprivate void configure(MergedContextConfiguration mergedConfig, SpringApplication application) { // 设置容器主类为测试类 application.setMainApplicationClass(mergedConfig.getTestClass()); // 设置容器组件加载源 application.addPrimarySources(Arrays.asList(mergedConfig.getClasses())); application.getSources().addAll(Arrays.asList(mergedConfig.getLocations())); // 获取初始化器 List<ApplicationContextInitializer<?>> initializers = getInitializers(mergedConfig, application); // 根据配置设置容器类型 if (mergedConfig instanceof WebMergedContextConfiguration) { application.setWebApplicationType(WebApplicationType.SERVLET); if (!isEmbeddedWebEnvironment(mergedConfig)) { new WebConfigurer().configure(mergedConfig, initializers); } } else if (mergedConfig instanceof ReactiveWebMergedContextConfiguration) { application.setWebApplicationType(WebApplicationType.REACTIVE); } else { application.setWebApplicationType(WebApplicationType.NONE); } // 设置容器提供工厂 application.setApplicationContextFactory(getApplicationContextFactory(mergedConfig)); // 如果当前是子容器,则关闭banner打印 if (mergedConfig.getParent() != null) { application.setBannerMode(Banner.Mode.OFF); } // 设置初始化器 application.setInitializers(initializers); // 获取容器环境,默认为null ConfigurableEnvironment environment = getEnvironment(); if (environment != null) { prepareEnvironment(mergedConfig, application, environment, false); application.setEnvironment(environment); } else { // 添加环境已准备好监听器,用于加载测试环境变量 application.addListeners(new PrepareEnvironmentListener(mergedConfig)); } }第10-11行,启动容器;注意,这里使用了
ApplicationContextHook,这是一个函数式接口,用来获取容器运行监听器:java@FunctionalInterface public interface SpringApplicationHook { SpringApplicationRunListener getRunListener(SpringApplication springApplication); }在
ContextLoaderHook.run()中,实际调用的是Application.withHook()方法:javapublic static <T> T withHook(SpringApplicationHook hook, ThrowingSupplier<T> action) { applicationHook.set(hook); try { return action.get(); } finally { applicationHook.remove(); } }它会在线程上下文中存储hook。
在
SpringApplication.run()中,getRunListeners()会从hook中获取监听器:javaprivate SpringApplicationRunListeners getRunListeners(String[] args) { ArgumentResolver argumentResolver = ArgumentResolver.of(SpringApplication.class, this); argumentResolver = argumentResolver.and(String[].class, args); List<SpringApplicationRunListener> listeners = getSpringFactoriesInstances(SpringApplicationRunListener.class, argumentResolver); SpringApplicationHook hook = applicationHook.get(); SpringApplicationRunListener hookListener = (hook != null) ? hook.getRunListener(this) : null; if (hookListener != null) { listeners = new ArrayList<>(listeners); listeners.add(hookListener); } return new SpringApplicationRunListeners(logger, listeners, this.applicationStartup); }之后,容器启动过程中,不同的监听器执行不同的动作。
ContextLoaderHook主要是为下面的useMainMethod准备的。
1.3 useMainMethod属性
通常,在代码结构组织良好的项目中,@SpringBootTest测试类会找到@SpringBootApplication主类,用于创建容器,该主类一般也包含main方法:
java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}一般情况下,在main方法中我们不会对容器做修改,但是,也有一些特殊的情况需要在main方法中对容器进行配置:
java
import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(MyApplication.class);
application.setBannerMode(Banner.Mode.OFF);
application.setAdditionalProfiles("myprofile");
application.run(args);
}
}如果我们想在测试容器中保留main方法的容器设置,那么我们可以使用@SpringBootTest的useMainMethod属性:
NEVER:默认值,意味着由Spring Boot Test框架创建容器,而不是调用主类的main方法来创建容器;ALWAYS:总是调用主类的main方法来创建容器;如果主类没有main方法或者找不到@SpringBootConfiguration注解配置的类,会抛出错误;WHEN_AVAILABLE:如果可以,就调用主类的main方法创建容器,如果不行,就由Spring Boot Test框架创建容器;
当使用useMainMethd后,创建容器的过程就不一样了,完整的loadContext()方法如下:
java
private ApplicationContext loadContext(MergedContextConfiguration mergedConfig, Mode mode,
ApplicationContextInitializer<ConfigurableApplicationContext> initializer, RuntimeHints runtimeHints)
throws Exception {
assertHasClassesOrLocations(mergedConfig);
SpringBootTestAnnotation annotation = SpringBootTestAnnotation.get(mergedConfig);
String[] args = annotation.getArgs();
UseMainMethod useMainMethod = annotation.getUseMainMethod();
Method mainMethod = getMainMethod(mergedConfig, useMainMethod);
if (mainMethod != null) {
if (runtimeHints != null) {
runtimeHints.reflection().registerMethod(mainMethod, ExecutableMode.INVOKE);
}
ContextLoaderHook hook = new ContextLoaderHook(mode, initializer,
(application) -> configure(mergedConfig, application));
return hook.runMain(() -> ReflectionUtils.invokeMethod(mainMethod, null, new Object[] { args }));
}
SpringApplication application = getSpringApplication();
configure(mergedConfig, application);
ContextLoaderHook hook = new ContextLoaderHook(mode, initializer, ALREADY_CONFIGURED);
return hook.run(() -> application.run(args));
}如果是由主类的main方法来创建容器,那么Spring Boot Test是如何影响容器创建过程的呢?答案就是SpringApplicationHook。
在第13-14行,创建ContextLoaderHook时,第三个参数Consumer<SpringApplication> configurer传入的是configure(mergedConfig, application),这就之前说的容器配置关键方法:
java
ContextLoaderHook(Mode mode, ApplicationContextInitializer<ConfigurableApplicationContext> initializer,
Consumer<SpringApplication> configurer) {
this.mode = mode;
this.initializer = initializer;
this.configurer = configurer;
}之后,在容器创建过程中Application.run(),获取运行监听器,会获取到hook中的监听器:
java
@Override
public SpringApplicationRunListener getRunListener(SpringApplication application) {
return new SpringApplicationRunListener() {
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
ContextLoaderHook.this.configurer.accept(application);
if (ContextLoaderHook.this.mode == Mode.AOT_RUNTIME) {
application.addInitializers(
(AotApplicationContextInitializer<?>) ContextLoaderHook.this.initializer::initialize);
}
}
@Override
public void contextLoaded(ConfigurableApplicationContext context) {
ContextLoaderHook.this.contexts.add(context);
if (ContextLoaderHook.this.mode == Mode.AOT_PROCESSING) {
throw new AbandonedRunException(context);
}
}
@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
ContextLoaderHook.this.failedContexts.add(context);
}
};
}在容器启动前,会执行ContextLoaderHook.this.configurer.accept(application);,实际就是执行SpringBootContextLoader.configure()方法,所以即使使用主类的main方法,Spring Boot Test也通过hook机制影响了容器创建过程。
1.4 webEnvironment
默认情况下,@SpringBootTest不会启动服务器(也就是说不会监听端口),我们可以使用webEnvironment属性来控制:
MOCK:默认值,启动一个WEB容器,并提供一个模拟的网络环境,内置服务器(tomcat)不会启动。如果在类路径下没有网络环境,那么会启动一个常规容器;实际创建的是
org.springframework.web.context.support.GenericWebApplicationContext容器;RANDOM_PORT:启动一个WEB容器,并提供一个真实的网络环境,内置服务器会启动,在随机端口上监听;实际创建的是
org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext容器;DEFINED_PORT:启动一个WEB容器,并提供一个真实的网络环境,内置服务器会启动,在指定端口(默认为8080)上监听,可以通过server.port更改;实际创建的是
org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext容器;NONE:启动一个常规容器,不提供网络环境;实际创建的是
org.springframework.context.annotation.AnnotationConfigApplicationContext容器;
在GenericWebApplicationContext中,实际是存在WEB相关的组件的,只是并没有启动服务器,在onRefresh()方法中,没有启动容器的代码:
java
@Override
protected void onRefresh() {
this.themeSource = UiApplicationContextUtils.initThemeSource(this);
}如果我们自己在测试类中启动容器,也是可以的:
java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
public class Demo {
@Test
public void test01(@Autowired ApplicationContext applicationContext) {
TomcatServletWebServerFactory webServerFactory = applicationContext.getBean(TomcatServletWebServerFactory.class);
WebServer webServer = webServerFactory.getWebServer();
webServer.start();
}
}输出如下:
txt
7778 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer -- Tomcat initialized with port 8888 (http)
7797 [main] INFO o.a.coyote.http11.Http11NioProtocol -- Initializing ProtocolHandler ["http-nio-8888"]
7800 [main] INFO o.a.catalina.core.StandardService -- Starting service [Tomcat]
7801 [main] INFO o.a.catalina.core.StandardEngine -- Starting Servlet engine: [Apache Tomcat/10.1.46]
7953 [main] INFO o.a.coyote.http11.Http11NioProtocol -- Starting ProtocolHandler ["http-nio-8888"]
7968 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer -- Tomcat started on port 8888 (http) with context path '/'可见我们也能自己启动服务器。
如果使用RANDOM_PORT或DEFINED_PORT,启动的容器是AnnotationConfigServletWebServerApplicationContext,在其父类ServletWebServerApplicationContext的onFresh()中,会创建WebServer:
java
@Override
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}在createWebServer中,并没有直接启动服务器,而是注册了一个WebServerStartStopLifecycle生命周期,在SpringApplication.run()中,会调用finishRefresh(),在其中就会启动服务器。
Spring 容器的 refresh() 是一个严谨的流水线。如果在 onRefresh(中途)就启动服务器并接收 HTTP 请求,此时容器里的 Bean 可能还没完全初始化好(比如 AOP 代理没挂载、后置处理器没跑完)。如果请求进来了,可能会导致空指针或逻辑错误。所以要在整个容器初始化完成后在启动服务器。
1.5 MockMvc
当@SpringBootTest启动的是WEB容器(不管是真实的服务器,还是模拟的服务器)时,如果我们要通过接口测试,可以使用MockMvc,在Spring Boot Test中,可以使用@AutoConfigureMockMvc,向容器中自动添加MockMvc支持,如下:
java
@SpringBootTest
@AutoConfigureMockMvc
class MyMockMvcTests {
// 使用MockMvc
@Test
void testWithMockMvc(@Autowired MockMvc mvc) throws Exception {
mvc.perform(get("/"))
.andExpect(status().isOk())
.andExpect(content().string("Hello World"));
}
// 使用MockMvcTester
@Test
void testWithMockMvcTester(@Autowired MockMvcTester mvc) {
assertThat(mvc.get().uri("/"))
.hasStatusOk()
.hasBodyTextEqualTo("Hello World");
}
}1.6 @TestsConfiguration
@TestConfiguration的作用是向容器中添加Bean。
用法一:作为内部类,那么只有该测试类的容器才有添加的Bean:
java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class Demo {
@Test
public void test01(@Autowired ApplicationContext applicationContext) {
for (String beanDefinitionName : applicationContext.getBeanDefinitionNames()) {
System.out.println(beanDefinitionName);
}
}
@TestConfiguration
public static class Config {
}
}用法二:作为外部类,如果想要某个测试类的容器有添加的Bean,需要使用@Import显式引入:
java
@TestConfiguration
public class ExternalConfig {
@Bean
public MockEmailService mockEmailService() {
// 模拟邮件发送,防止测试时真的发出邮件
return new MockEmailService();
}
}java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@Import(ExternalConfig.class)
public class Demo {
@Test
public void test01(@Autowired ApplicationContext applicationContext) {
for (String beanDefinitionName : applicationContext.getBeanDefinitionNames()) {
System.out.println(beanDefinitionName);
}
}
}下面是@Configuration与@TestConfiguration的对比:
| 特性 | @Configuration | @TestConfiguration |
|---|---|---|
| 组件扫描 | 会被 @SpringBootApplication 扫描并加载进容器。 | 不会被自动扫描加载。 |
| 设计意图 | 定义生产环境的 Bean。 | 定义测试专用的 Bean 或覆盖生产 Bean(需要允许Bean覆盖)。 |
| 内部类 | 称为测试类容器的主配置 | 主配置的补充,向容器加入其他组件 |
2. 切片测试
2.1 什么是切片测试
当我们使用@SpringBootTest时,默认会加载整个程序的容器配置,也就是说会加载所有组件,如果我们只是测试一个简单的类,而花费资源来加载庞大的容器,显然是不合算的。因此,容器能不能只加载与测试有关的组件,或者说,当我们测试程序某一层时(例如Controller层、Dao层、Service层),只加载与这一层有关的组件,这就是切片测试。
例如,当我们只想测试Controller层时,我们只想验证路径映射对不对、参数绑定对不对、JSON序列化与反序列化对不对,我们不想执行数据库操作,因此,与数据库相关的组件是不需要加载的,我们只需要加载Controller层相关的组件,这就是切片测试。
在Spring Boot Test中,提供了很多@…Test注解,来加载某一层的组件,这些注解的工作方式相似,首先禁用程序的自动配置,然后加载容器,加载有关的组件。
2.2 实现
我们以@WebMvcTest为例,讲解切片测试是如何实现的。部分源码如下:
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(WebMvcTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(WebMvcTypeExcludeFilter.class)
@AutoConfigureCache
@AutoConfigureWebMvc
@AutoConfigureMockMvc
@ImportAutoConfiguration
public @interface WebMvcTest {
}2.2.1 @BootstrapWith(WebMvcTestContextBootstrapper.class)
WebMvcTestContextBootstrapper继承自SpringBootTestContextBootstrapper,是Spring TestContext Framework启动器的实现者,主要有两个作用:
- 定义容器类型为WEB容器;
- 从
@WebMvcTest的properties属性值中提取环境变量;
java
class WebMvcTestContextBootstrapper extends SpringBootTestContextBootstrapper {
@Override
protected MergedContextConfiguration processMergedContextConfiguration(MergedContextConfiguration mergedConfig) {
MergedContextConfiguration processedMergedConfiguration = super.processMergedContextConfiguration(mergedConfig);
return new WebMergedContextConfiguration(processedMergedConfiguration, determineResourceBasePath(mergedConfig));
}
@Override
protected String[] getProperties(Class<?> testClass) {
WebMvcTest webMvcTest = TestContextAnnotationUtils.findMergedAnnotation(testClass, WebMvcTest.class);
return (webMvcTest != null) ? webMvcTest.properties() : null;
}
}2.2.2 @OverrideAutoConfiguration(enabled = false)
WebMvcTestContextBootstrapper继承自SpringBootTestContextBootstrapper,并且没有改变查找容器的方式,那么默认情况下,@WebMvcTest还是会找到主类@SpringBootApplication:
java
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
}@EnableAutoConfiguration的作用是根据项目类路径(Classpath)下的 Jar 包依赖,自动猜测并配置可能需要的组件,如果让这个注解生效,那么整个容器就会变得非常庞大。
因此,@OverrideAutoConfiguration(enabled = false)就是禁用自动配置的。
2.2.3 @TypeExcludeFilters(WebMvcTypeExcludeFilter.class)
即使禁用了@EnableAutoConfiguration,但是@SpringBootApplication上还有@ComponentScan,这个注解默认会扫描我们程序中的组件,也就是说@Controller、@Service、@Component等不同类型的组件都会被扫描进容器,而我们只想测试WEB层,因此,只需要WEB相关的组件,例如@Controller、@ControllerAdvice等。
因此,@TypeExcludeFilters(WebMvcTypeExcludeFilter.class)就是排除我们项目中不想要的组件,例如@Service、@Repository等。
注意,WebMvcTypeExcludeFilter.class不会把
@Configuration排除。
2.2.4 @ImportAutoConfiguration
当我们禁用自动加载后,与WEB相关的组件就不会加载了,但是要想测试WEB层,仍然需要加载相关的WEB组件,@ImportAutoConfiguration实现了按需加载。
方式 A:显式指定类名(直接点名)。这种方式最直观,告诉 Spring 需要哪几个自动配置类:
java
@ImportAutoConfiguration({
JdbcTemplateAutoConfiguration.class,
DataSourceAutoConfiguration.class
})
class MyDbTest { ... }方式 B:基于注解的自动关联(隐式加载)。当注解本身没有写参数时,它会执行按名查找逻辑:
- 查找当前注解的全类名(例如
org.example.MyTestAnnotation); - 去
META-INF/spring/下找同名的.imports文件,如META-INF/spring/org.example.MyTestAnnotation.imports文件; - 读取该文件中的类列表并自动加载;
实际上,@WebMvcTest上的注解并没有对应的META-INF/spring/org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest.imports文件。
主要是通过三个@AutoConfigureXXX注解来实现自动加载依赖的。
2.2.5 @AutoConfigure...
查看@AutoConfigureWebMvc部分源码:
java
@AutoConfigureJson
@ImportAutoConfiguration
public @interface AutoConfigureWebMvc {
}又自动引入了JSON相关组件,因此,该注解最终会去以下地方查找自动配置类:
txt
org.springframework.boot.test.autoconfigure.json.AutoConfigureJson.imports
org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc.imports以JSON为例,最终会根据以下三个自动配置类导入组件:
txt
# AutoConfigureJson auto-configuration imports
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration
org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration查看@AutoConfigureMockMvc部分源码:
java
@ImportAutoConfiguration
@PropertyMapping("spring.test.mockmvc")
public @interface AutoConfigureMockMvc {
}@PropertyMapping的作用是把测试注解的配置映射到相关环境变量上,例如
java
@Retention(RUNTIME)
@PropertyMapping("my.example")
public @interface Example {
String name();
}当在测试类上使用@Example注解时:
java
@Example(name="Spring")
public class MyTest {
}会产生一个名为my.example.name的属性,值为Spring。
2.3 @WebMvcTest
如果在测试类上注解@WebMvcTest,那么Spring Boot Test会启动WEB切片测试,加载项目所有的Controller。
如果只想加载某一个或某几个Controller,可以在@WebMvcTest中指定:
java
@WebMvcTest({CalculatorController.class}) // 明确指定 Controller,避免扫描整个包
class CalculatorControllerTest {
@Autowired
private MockMvc mockMvc; // 由 MockMvcAutoConfiguration 自动注入
@MockitoBean
private CalculatorService calculatorService; // 必须手动 Mock,因为 Service 被 Filter 拦截了
@Test
void testAdd() throws Exception {
// 给 Mock 对象设置行为
given(calculatorService.add(10, 20)).willReturn(30);
mockMvc.perform(get("/add").param("num1", "10").param("num2", "20"))
.andExpect(status().isOk())
.andExpect(content().string("30"));
}
}WARNING
如果测试类 A 使用了 @MockitoBean(ServiceA.class),而测试类 B 使用了 @MockitoBean(ServiceA.class) 和 @MockitoBean(ServiceB.class),Spring 会认为这是两个不同的上下文,从而启动两次容器。
最佳实践:创建一个公共的父类或者配置类,统一声明所有的 @MockitoBean,这样整个测试套件可以复用同一个微缩容器。
默认情况下,@COnfiguration也会加载进容器,如果想排除项目中的配置类,可以使用以下方式:
java
@WebMvcTest(
controllers = {CalculatorController.class},
excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = AppConfig.class)}
)
public class DemoTest {
}参考资料
[1] Spring Boot Test: https://docs.spring.io/spring-boot/reference/testing/spring-boot-applications.html