提起“单元测试”这几个字,一般开发者会条件反射般想起:“工作忙,没时间”,这是一个客观上的事实,在急于求成的大环境下,规范的单元测试却需要一些明确的代码产出及覆盖率指标,这的确很让人头疼。早几年的关于单元测试的文章,不停的鼓吹其好处,却对时间问题视而不见。

而与此相对: 对于开发者而言,其编写的代码是否需要经过测试,经过几轮测试才能让开发者安心?相信大多数开发者会说需要,且测试次数越多越好,毕竟上线时求神拜佛的滋味其实并不好受。

所以这其实是自相矛盾的,我们身处其中,有时候只能抱测试哥哥的大腿以求生路。但是仅仅靠测试人员把关,有些细节和异常流程不可避免会被漏掉。

1. 单元测试的缺点(抛砖引玉之言,不喜勿喷)

回到单元测试本身的概念上来,单元测试强调剥离所有外部依赖的影响,对类中的每个方法都写一个测试case,这里面本身存在一些现实问题:

  1. 为每个方法写测试case,时间成本太高
  2. 有些方法的操作本身非常简单,只是一些简单的赋值等操作,没有必要写测试
  3. 团队开发能力参差不齐,解耦做的不好的情况下,单元测试越发困难
  4. 有些依赖非常难以剥离(进行mock),或者剥离的代价比较大,比如mvc中Controller需要web容器,数据库访问需要真实数据库(使用内存数据库,初始化的工作量也非常大),Redis等

单元测试,仅适合那些逻辑复杂,逻辑分叉较多且较少依赖外部环境的方法,这些方法使用unit test再合适不过。除此之外的其他业务场景,建议舍弃单元测试,投入到“功能测试”的怀抱。

2. 功能测试

在本文中,我们对功能测试做一下约定:在单个java虚拟机内部的,mock大部分外部依赖的影响,针对业务功能(通常是Controller或对外公开的Service)的测试,称之为“功能测试”,把单个服务内部的业务功能综合在一起,每一个测试case都是一个小业务流程。

测试金字塔

测试金字塔中的第二层是我们重点关注的,unit test虽好,但常规的业务开发中用的不多。

功能测试不同于集成测试(UI测试),集成测试原意是强调端到端的完整链路测试,期望环境尽可能是真实的,每一个测试case都是一个完整的业务流程,本文不讨论集成测试相关内容。

用测试代码安安心心的写出一条功能测试case,确保它能够正确执行,每个核心业务功能一条测试case。这样仍需做一些mock工作,但mock工作量变小了很多,再加上Spring框架支持,进一步减轻了测试工作量。

3. Spring Boot Test 简介

Spring Test与JUnit等其他测试框架结合起来,提供了便捷高效的测试手段。而Spring Boot Test 是在Spring Test之上的再次封装,增加了切片测试,增强了mock能力。

整体上,Spring Boot Test支持的测试种类,大致可以分为如下三类:

类别 描述 涉及的注解
单元测试 一般面向方法,编写一般业务代码时,测试成本较大(理由见上文) @Test
切片测试 一般面向难于测试的边界功能,介于单元测试和功能测试之间 @RunWith @WebMvcTest等
功能测试 一般面向某个完整的业务功能,同时也可以使用切面测试中的mock能力,推荐使用 @RunWith @SpringBootTest等

功能测试过程中的几个关键要素及支撑方式如下:

要素 实现方式
测试运行环境 通过@RunWith 和 @SpringBootTest启动spring容器
mock能力 Mockito提供了强大mock功能
断言能力 AssertJ、Hamcrest、JsonPath提供了强大的断言能力

4. 快速开始

增加spring-boot-starter-test依赖,使用@RunWith和@SpringBootTest注解,即可开始测试。

4.1 添加依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>

一旦依赖了spring-boot-starter-test,下面这些类库将被一同依赖进去:

名称 简介
JUnit java测试事实上的标准,默认依赖版本是4.12(JUnit5和JUnit4差别比较大,集成方式有不同)
Spring Test & Spring Boot Test Spring的测试支持
AssertJ 提供了流式的断言方式
Hamcrest 提供了丰富的matcher
Mockito mock框架,可以按类型创建mock对象,可以根据方法参数指定特定的响应,也支持对于mock调用过程的断言
JSONassert 为JSON提供了断言功能
JsonPath 为JSON提供了XPATH功能

4.2 测试类

package com.example.learn.springboottestlearn.ttt;

import com.example.learn.springboottestlearn.entity.User;
import com.example.learn.springboottestlearn.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringBootTestLearnApplicationTests {

    @Autowired
    private UserService userService;

    @Test
    public void testAddUser() {
        User user = new User();
        user.setName("john");
        user.setAddress("earth");
        userService.add(user);
    }

}

@RunWith是Junit4提供的注解,将Spring和Junit链接了起来。

假如使用Junit5,不再需要使用@ExtendWith注解,@SpringBootTest和其它@*Test默认已经包含了该注解。

@SpringBootTest替代了spring-test中的@ContextConfiguration注解,目的是加载ApplicationContext,启动spring容器。

使用@SpringBootTest时并没有像@ContextConfiguration一样显示指定locations或classes属性,原因在于@SpringBootTest注解会自动检索程序的配置文件,检索顺序是从当前包开始,逐级向上查找被@SpringBootApplication或@SpringBootConfiguration注解的类。

5. 功能测试

一般情况下,使用@SpringBootTest后,Spring将加载所有被管理的bean,基本等同于启动了整个服务,此时便可以开始功能测试。

由于web服务是最常见的服务,且我们对于web服务的测试有一些特殊的期望,所以@SpringBootTest注解中,给出了webEnvironment参数指定了web的environment,该参数的值一共有四个可选值:

名称 说明
MOCK 此值为默认值,该类型提供一个mock环境,可以和@AutoConfigureMockMvc或@AutoConfigureWebTestClient搭配使用,开启Mock相关的功能。注意此时内嵌的服务(servlet容器)并没有真正启动,也不会监听web服务端口。
RANDOM_PORT 启动一个真实的web服务,监听一个随机端口。
DEFINED_PORT 启动一个真实的web服务,监听一个定义好的端口(从application.properties读取)。
NONE 启动一个非web的ApplicationContext,既不提供mock环境,也不提供真实的web服务。

另外,如果当前服务的classpath中没有包含web相关的依赖,spring将启动一个非web的ApplicationContext,此时的webEnvironment就没有什么意义了

6. 切片测试

所谓切片测试,官网文档称为 “slice” of your application,实际上是对一些特定组件的称呼。这里的slice并非单独的类(毕竟普通类只需要基于JUnit的单元测试即可),而是介于单元测试和集成测试中间的范围。

slice是指一些在特定环境下才能执行的模块,比如MVC中的Controller、JDBC数据库访问、Redis客户端等,这些模块大多脱离特定环境后不能独立运行,假如spring没有为此提供测试支持,开发者只能启动完整服务对这些模块进行测试,这在一些复杂的系统中非常不方便,所以spring为这些模块提供了测试支持,使开发者有能力单独对这些模块进行测试。

通过@*Test开启具体模块的测试支持,开启后spring仅加载相关的bean,无关内容不会被加载。

使用@WebMvcTest用来校验controllers是否正常工作的示例:

import org.junit.*;
import org.junit.runner.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.boot.test.autoconfigure.web.servlet.*;
import org.springframework.boot.test.mock.mockito.*;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.BDDMockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringRunner.class)
@WebMvcTest(UserVehicleController.class)
public class MyControllerTests {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private UserVehicleService userVehicleService;

    @Test
    public void testExample() throws Exception {
        given(this.userVehicleService.getVehicleDetails("sboot"))
                .willReturn(new VehicleDetails("Honda", "Civic"));
        this.mvc.perform(get("/sboot/vehicle").accept(MediaType.TEXT_PLAIN))
                .andExpect(status().isOk()).andExpect(content().string("Honda Civic"));
    }

}

使用@WebMvcTest和MockMvc搭配使用,可以在不启动web容器的情况下,对Controller进行测试。

7. 小结

本文主要介绍了如下几点内容:

  1. 测试可以分为单元测试、功能测试、以及介于两者之间的切片测试
  2. 建议放弃不必要的单元测试,拥抱功能测试、切片测试。
  3. Spring Boot Test在spring-test基础上,增强了mock能力,增加了测试的自动配置、切片测试。
  4. @SpringBootTest、@WebMvcTest等其他@*Test注解, 作为开启测试的注解,都可以启动一个ApplicationContext。

文章作者: 沉迷思考的鱼
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 沉迷思考的鱼 !
评论
 上一篇
Spring Boot Test (二、注解详解) Spring Boot Test (二、注解详解)
Spring为了避免的繁琐难懂的xml配置,引入大量annotation进行系统配置,确实减轻了配置工作量。由此,理解这些annotation变得尤为重要,一定程度上讲,对Spring Boot Test的使用,就是对其相关annotati
2018-11-20
本篇 
Spring Boot Test (一、快速入门) Spring Boot Test (一、快速入门)
提起“单元测试”这几个字,一般开发者会条件反射般想起:“工作忙,没时间”,这是一个客观上的事实,在急于求成的大环境下,规范的单元测试却需要一些明确的代码产出及覆盖率指标,这的确很让人头疼。早几年的关于单元测试的文章,不停的鼓吹其好处,却对时
2018-11-17
  目录