Skip to content

Spring Boot Test

本文介绍Spring Boot中对于测试的相关支持与特性。

1. @SprinBootTest注解

1.1 基础使用

@SpringBootTest注解用在测试类上,用于启动Spring Boot容器,例如:

java
@SpringBootTest
public class Demo {
    @Test
    public void test01() {}
}

所谓启动Spring Boot容器,如下:

java
SpringApplication.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);
        }
    
    }
  • valueproperties:字符串数组,以key=value格式定义测试环境变量;

  • args@SpringBootTest启动的是Spring Boot容器,归根结底,就是运行以下代码:

    java
    SpringApplication 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);,用来判断容器组件定义是否存在,如果不存在,则直接抛异常:

    java
    private 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

    java
    protected 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对象:

    java
    SpringApplication application = getSpringApplication();
    
    protected SpringApplication getSpringApplication() {
      return new SpringApplication();
    }
  • 第9行,配置容器,这是关键代码:

    java
    private 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()方法:

    java
    public static <T> T withHook(SpringApplicationHook hook, ThrowingSupplier<T> action) {
      applicationHook.set(hook);
      try {
        return action.get();
      }
      finally {
        applicationHook.remove();
      }
    }

    它会在线程上下文中存储hook。

    SpringApplication.run()中,getRunListeners()会从hook中获取监听器:

    java
    private 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方法的容器设置,那么我们可以使用@SpringBootTestuseMainMethod属性:

  • 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_PORTDEFINED_PORT,启动的容器是AnnotationConfigServletWebServerApplicationContext,在其父类ServletWebServerApplicationContextonFresh()中,会创建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容器;
  • @WebMvcTestproperties属性值中提取环境变量;
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:基于注解的自动关联(隐式加载)。当注解本身没有写参数时,它会执行按名查找逻辑:

  1. 查找当前注解的全类名(例如 org.example.MyTestAnnotation);
  2. META-INF/spring/ 下找同名的 .imports 文件,如META-INF/spring/org.example.MyTestAnnotation.imports文件;
  3. 读取该文件中的类列表并自动加载;

实际上,@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