Spring Boot 提供了许多注解和工具帮助开发人员测试应用,在其官方文档中也用了大量篇幅介绍单元测试的使用。在谷歌每周的 TGIF (ThanksGod, it's Friday)员工大会中有一项就是 宣布-周单元测试竞赛获胜的工程师。谷歌之所以这么重视单元测试,就是为了保证程序质量,鼓励大家多写测试代码。国内大多数开发人员对单元测试有所忽视,这也是我写本章内容的原因所在。
本章会围绕 Spring Boot 对单元测试的支持、常用单元测试功能的使用实例以及 MockMvc的自动配置机制展开。
Spring Boot 对单元测试的支持重点在于提供了-系列注解和工具的集成,它们是通过两个项目提 供 的 : 包 含 核 心 功 能 的 spring-boot-test 项 目 和 支 持 自 动 配 置 的spring-boot-test-autoconfigure.
通常情况下,我们通过 spring-boot-starter-test 的 Starter 来引入 SpringBoot 的核心支持项目以及单元测试库。spring-boot-starter-test 包 含的类库如 JUnit:一个 Java 语言的单元测试框架。
Spring Test & Spring Boot Test:为 Spring Boot 应用提供集成测试和工具支持。
AssertJ:支持流式断言的 Java 测试框架。
.Hamcrest: 一个匹配器库。
Mockito :一个 Java Mock 框架。
JSONassert:一个针对 JSON 的断言库。
JsonPath:一个 JSON XPath 库。
如果 Spring Boot 提供的基础类库无法满足业务需求,我们也可以自行添加依赖。依赖注入的优点之一就是可以轻松使用单元测试。 这种方式可以直接通过 new 来创建对象,而不需要涉及 Spring。当然,也可以通过模拟对象来替换真实依赖。
如果需要集成测试,比如使用 Spring 的 ApplicationContext, Spring 同样能够提供无须部署应 用 程 序 或 连 接 到 其 他 基 础 环 境 的 集 成 测 试 。 而 SpringBoot 应 用 本 身 就 是 一 个ApplicationContext,因此除了正常使用 Spring.上下文进行测试,无须执行其他操作。
以 Junit 为例,在单元测试中会常用到一些注解,比如 Spring Boot 提供的@SpringBootTest
@MockBean、@SpyBean 、@WebMvcTest@AutoConfigureMockMvc 以及 Junit 提供的@RunWith 等。下面以- 一个简单的订单插入的功能示例进行说明。
@RunWith(SpringRunner .class)public class OrderServiceTest {@Autowiredprivate OrderService orderService;ublic void testInsert() {Order order = new Order()order . setOrderNo("A001");order. setUserId(100);orderService. insert (order);}}
我们先来看 Junit 中的@RunWith 注解,该注解用于说明此测试类的运行者,比如示例中使用 的 SpringRunner 。 SpringRunner 是 由 spring-test 提 供 的 , 它 实 际 上 继 承 了SpringJUnit4ClassRunner 类,并且未重新定义任何方法,我们可以将 SpringRunner 理解为 SpringJUnit4ClassRunner 更简洁的名字。
@SpringBootTest 注解由 Spring Boot 提供,该注解为 SpringApplication 创建上下文并支持 Spring Boot 特性。
该测试项目中引入了 spring-boot-starter-test 依赖,默认情况下此依赖使用的单元测试类库为 J∪nit4,此时@SpringBootTest 注解需要配合@RunWith(SpringRunner.class)注解使用,否则注解会被忽略。
查看@SpringBootTest 注解的源码,会发现其内部枚举类 WebEnvironment 提供了支持的多种单元测试模式。
@Target(ElementType. TYPE)@Retention(Retent ionPolicy . RUNTIME)@Documented@Inherited@BootstrapWith(SpringBootTestContextBootstrapper. class)@ExtendWith(SpringExtension. class)public @interface SpringBootTest {@AliasFor("properties")String[] value() default {};@AliasFor("value" )String[] properties() default {};String[] args() default {};Class>[] classes() default {};WebEnvi ronment webEnvironment() default WebEnvironment . MOCK;enum WebEnvironment {MOCK(false),RANDOM PORT(true)DEFINED_ PORT(true),NONE(false); } }
从@SpringBootTest 的源代码中可以看出,通过 WebEnvironment 枚举类提供了 MOCK、RANDOM_ PORT、DEFINED_ PORT 和 NONE 这 4 种环境配置。
:Mock:加载 WebApplicationContext 并提供 Mock Servlet 环境,嵌入的 Servlet 容器不会被启动。
:RANDOM_ PORT:加载一个 EmbeddedWebApplicationContext 并提供真实的 Servlet 环境。嵌入的 Servlet 容器将被启动,并在一个随机端口上监听。
:DEFINED_ PORT:加载一个 EmbeddedWebApplicationContext 并提供真实的 Servlet 环境。嵌入的 Servlet 容器将被启动,并在一个默认的端口上监听(application.properties 配 置端口或者默认端口 8o8o)。
:NONE:使用 SpringApplication 加载一个 ApplicationContext,但是不提供任何 Servlet 环境。
示例中默认采用此种方式。
关于其他的注解就不再展开了,在后面章节中会结合具体示例进行说明。
在上节中已经提到 JUnit5 与 JUnit4 有所不同,本节还是用同样的示例来看一下 JUnit5 的使用。
@SpringBootTestpublic class OrderServiceTest {@Resourceprivate OrderService orderService;@Testpublic void testInsert()Order order = new Order();order . setOrderNo( "A001");order . setUserId(100);orderService . insert (order);}
通过上面的代码,我们可以看出默认情况下只需要使用@SpringBootTest 注解即可,而在上节@SpringBootTest 源代码中已经看到组合了@ExtendWith(SpringExtension.class)注解,因此此示例无须注解。
这里需要注意的是 Spring Boot 的版本信息,在 2.1.x 之后@SpringBootTest 注解中才组合了@ExtendWith(SpringExtension.class)注解。因此,需要根据具体使用的版本来确定是否需要@ExtendWith(SpringExtension.class)注解,否则可能会出现注解无效的情况虽然单元测试类的代码与 JUnit4 基本相同,但本质上还是有区别的。比如,在使用 JUnit5时, 默认的 spring-boot- starter-test 依赖类库已经无法满足,需要手动引|入 junit-jupiter.
org . junit. jupiter groupId>junit - jupiter artifactId>5.5.2test/ dependency>
同时,如果必要则需要将 junit-vintage-engine 进行排除。
org. springframework . bootspring-boot-starter-testtestorg. junit . vintage groupId>junit -vintage - engine
上面的测试代码还有一个经常会遇到的问题,就是从 JUnit4 升级到 JUnit5 时,如果你只是把类上的注解换了,会发现通过@Resource 或@Autowired 注入的 OrderService 会抛出空指针异常。这是为什么呢?
原因很简单,从 JUnit4 升级到 JUnit5 时,在 testInsert 方法 上的@Test 注解变了。在 JUnit4中默认使用的@Test 注解为 org.junit.Test,而在 JUnit5 中需要使用 org.junit.jupiter.api.Test.因此,如果在升级的过程中出现莫名其妙的空指针异常时,需考虑到此处。
总体来说,JUnit5 的最大变化是 @Test 注解改为由几个不同的模块组成,其中包括 3 个不同子项目: JUnit Platform、JUnit Jupiter 和 JUnit Vintage.同时,JUnit5 也提供了一套自己的注解。
.@ BeforeAll 类似于 JUnit 4 的@BeforeAll,表示使用了该注解的方法应该在当前类中所有使用了@Test、@ RepeatedTest、@ ParameterizedTest 或者@TestFactory 注解的方法之前执行,且必须为 static。
.@ BeforeEach 类似于 JUnit 4 的@Before,表示使用了该注解的方法应该在当前类中所有使用了@Test、@ RepeatedTest、@ ParameterizedTest 或者@TestFactory 注解的方法之前执行。
.@Test 表示该方法是一个测试方法。
.@ DisplayName 为测试类或测试方法声明一个自定义的显示名称。
.@AfterEach 类似于 JUnit 4 的@After,表示使用了该注解的方法应该在当前类中所有使用了@Test、@RepeatedTest 、@ ParameterizedTest 或者@ TestFactory 注解的方法之后执行。
.@AfterAll 类似于 JUnit 4 的@AfterClass, 表示使用了该注解的方法应该在当前类中所有使用了@Test、@RepeatedTest、 @ ParameterizedTest 或者@ TestFactory 注解的方法之后执行,且必须为 static。
.@Disable 用于禁用一个测试类或测试方法,类似于 JUnit 4 的@Ignore.
.@ExtendWith 用于注册自定义扩展功能。
关于这些注解的详细使用,我们就不一一举例了。
在面向对象的程序设计中,模拟对象(mock object)是以可控的方式模拟真实对象行为的假对象。在编程过程中,通常通过模拟一些输入数据,来验证程序是否达到预期效果。
模拟对象-般应用于真实对象有以下特性的场景:行为不确定、真实环境难搭建、行为难触发、速度很慢、需界面操作、回调机制等。
在上面章节中实现了 Service 层的单元测试示例,而当对 Controller 层进行单元测试时,便需要使用模拟对象,这里采用 spring-test 包中提供的 MockMvc。MockMvc 可以做到不启动项目工程就可以对接口进行测试。
MockMvc 实现了对 HTTP 请求的模拟,能够直接使用网络的形式,转换到 Controller 的调用,这样可以使得测试速度快、不依赖网络环境,同时提供了一套验证的工具, 使得请求的验证统-一而且方便。
下面以一个具体的示例来对 MockMvc 的使用进行讲解。在使用之前,依旧需要先引入对应的依赖。
org. springfr amework . boot groupId>spring boot - starter- test artifactId>test dependency>这里创建一一个简单的 TestController,提供一个 hello 方法, 返回一个字符串。@RestControllerpublic class TestController {@RequestMapping(" /mock" )public String mock(String name) {return "Hello ”+ name + "!"; }}
下面编写单元测试的类和方法,我们这里都采用基于 JUnit4 和 SpringBoot 2.x 版本进行操作。
@RunWith(SpringRunner . class)@SpringBootTest@AutoConfigureMockMvcpublic class TestControllerTest {@Autowiredprivate MockMvc mockMvc;@Testpublic void testMock() throws Exception {// mockMvc. perform 执行一 个请求mockMvc . perform(MockMvcRequestBuilders// MockMvcRequestBuilders. get( "XX")构造一个请求. get(" /mock")//设置返回值类型为 utf-8, 否则默认为 ISO- 8859-1. accept (MediaType . APPLICATION_ JSON_ _UTF8_ _VALUE)/ ResultActions . param 添加请求传值. param( "name", "MockMvc"))// Resul tActions . andExpect 添加执行完成后的断言. andExpect (MockMvcResultMatchers . status(). isOk()). andExpect (MockMvcResultMatchers . content(). string("Hello MockMvc!"))// Resul tActions . andDo 添加一一个结果处理器,此处打印整个响应结果信息. andDo(MockMvcResultHandlers . print());}}
执行该单元测试打印结果部分内容如下。
MockHttpServletRequest:HTTP MethodGETRequest URI = /mockParameters = {name=[MockMvc]}Headers = [Accept:" application/json; charset=UTF-8" ]Body = nullSession Attrs = {}Handler:Type = com. secbro2. learn. controller. TestControllerMethod = public java. lang . String com. secbro2 . learn. controller .TestController .mock(java. lang. String)MockHttpServletResponse:Status = 200Error message = nulHeaders = [Content -Type :"application/json;charset=UTF-8", Content-Length:"14" ]Content type = application/json;charset-UTF-8Body = Hello MockMvc!
在以上单元测试中,@RunWith(SpringRunner. class )和@SpringBootTest 的作用我们已经知道,另外的@AutoConfigureMockMvc 注解提供了自动配置 MockMvc 的功能。 因此,只需通过@Autowired 注入 MockMvc 即可。
MockMvc 对象也可以通过接口 MockMvcBuilder 的实现类来获得。该接口提供一个唯一的build 方法来构造 MockMvc。主要有两个实现类:
StandaloneMockMvcBuilder 和 DefaultMockMvcBuilder,分别对应两种测试方式,即独立安装和集成 Web 环境测试(并不会集成真正的 web 环境,而是通过相应的 Mock API 进行模拟测 试 , 无 须 启 动 服 务 器 ) 。 MockMvcBuilders 提 供 了 对 应 的 standaloneSetup 和webAppContextSetup 两种创建方法,在使用时直接调用即可。MockMvc 对象的创建默认使用 DefaultMockMvcBuilder,后面章节会详细介绍这一过程。
整个单元测试包含以下步骤:准备测试环境、执行 MockMvc 请求、 添加验证断言、添加结果处理器、得到 MvcResult 进行自定义断言/进行下一步的异步请求、卸载测试环境。
关于 Web 应用的测试,还有许多其他内容,比如:检测 Web 类型、检测测试配置、排除测试配置以及事务回滚(通过@ Transactional 注解),读者朋友可根据需要自行编写单元测试用例进行尝试。