系列目录
引言
这篇文章文中的实用例子只是一个抛砖引玉的作用。
适合新手学习,或者时间充裕可以深入研究以这篇为目录进行查漏补缺。
了解单元测试
单元测试属于小型测试,针对单个函数的测试,关注其内部逻辑输出的结果是否正确。如果将一个单元测试看成是一个单位,只需保证每一个单元测试都通过,则可以大大提高项目质量。单元测试可以保证能够代码覆盖率达到100%的测试。
但是我们往往在开发中都不愿意好好写单元测试,理由有很多,绝大多数如下:
- 需求赶,没有足够的时间写单元测试
- 功能需求太简单,没有必要写单元测试
- 当需求变动的时候,又要修改单元测试,增加了开发时间
- 应该交给测试人员来完成
...
以上的问题,其实对于每一个项目普遍存在。在以前,我也是这样的心态,不愿意写单元测试。但当我尝试了几次单例的带来的甜头后,越发喜欢和习惯写单元测试。我觉得当你认识到单元测试的意义,以及熟悉使用单元测试,你自然会打消以上的疑虑并且爱上它。
单元测试的意义
- 它可以保证你写的代码是你想要的结果。这个点很重要,因为在编程中,经常会敲错代码导致结果并不是自己脑子里想的。如果不经过单元测试测试下运行结果,那么代码质量是肯定保证不了的。
- 单元测试是最少单位,一个高可用的系统需要靠一个一个最小的稳定的单位组成。所以保证一个最小单位的准确率是必须的。
- 单元测试应该是快速的,因此它不应该使用任何Web服务器。
- 每个单元测试应该独立于其他测试。
- 当出现问题的时候,单元测试可以很快帮助你排查问题。因为单元测试保证你写的代码是你想要的结果,当出现异常效果,只需要从对应的单元测试是排查,就可以很快定位问题。
如何实现单元测试
在讨论如果实现单元测试的之前,我们要先想想,什么是好的单元测试呢?
- 完整性:覆盖率高,意思就是对各种情况都要考虑到
- 健壮性:具有健壮性的单元测试,完全不需要被修改或者只有极少的修改。因为单元测试只是关注输出结果是否符合期望,如果只是修改了实现逻辑,那么单元测试是不需要改动的。
- 粒度细:其实这里跟代码的设计和实现有关。考虑到单测实现的简洁,把各个功能分成每个函数,保证粒度足够细。(评判代码或者设计好不好的⼀个准则是看它容不容易测试)
那么接下来我们要讨论下需要测试什么?
上文已经提到,单元测试测试是最小粒度的代码,通常是一个方法或函数。通常是通过⼀系列不同的⾏为。⾏为就是对不同的输⼊场景有不同的输出,每⼀个⾏为都需要独⽴的单测。
实战
接下来,我们来讨论一下如何写单元测试
如何保证单元测试细粒度
在实战前,我们要考虑如何保证单元测试的细粒度呢?
在绝大多数业务中,单个方法/函数也是有调用其他方法/函数的,那么当我们测试的方法调用链很深的时候,这相当于测试用例的粒度变大了,返回的结果情况也会因为调用链的深度而变复杂。
又或者测试的方法/函数有调用远程数据源或者远程接口,这种情况往往测试依赖性很高。如果数据库没有准备好,或者远程接口不允许测试,那么单元测试就没办法进行下去。这样是打击了写单元测试的热情。
以上情况,其实我们通常会用内嵌数据库或者Mock来解决。下面就来介绍一下他们的用途
内嵌数据库
在开发应用的过程中使用内嵌的内存数据库是非常方便的,很明显,内存数据库不提供数据的持久化存储;当应用启动时你需要填充你的数据库,当应用结束时数据将会丢弃
内嵌数据库一般使用
Mysql
:H2
MongoDB
:fongo
Redis
:embedded-redis
例子
此处结合Mybatis-plus的初始化工程看看如何使用H2内嵌数据库
添加依赖
com.h2database
h2
runtime
com.baomidou
mybatis-plus-boot-starter
配置
# DataSource Config
spring:
datasource:
driver-class-name: org.h2.Driver
schema: classpath:db/schema-h2.sql
data: classpath:db/data-h2.sql
url: jdbc:h2:mem:test
username: root
password: test
initialization-mode: always
# Logger Config
logging:
level:
com.baomidou.mybatisplus.samples.quickstart: debug
schema-h2.sql
DROP TABLE IF EXISTS user;
CREATE TABLE user
(
id BIGINT(20) NOT NULL COMMENT '主键ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
age INT(11) NULL DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (id)
);
data-h2.sql
DELETE FROM user;
INSERT INTO user (id, name, age, email) VALUES
(1, 'Jone', 18, '[email protected]'),
(2, 'Jack', 20, '[email protected]'),
(3, 'Tom', 28, '[email protected]'),
(4, 'Sandy', 21, '[email protected]'),
(5, 'Billie', 24, '[email protected]');
Spring boot启动类添加@MapperScan
注解
@SpringBootApplication
@MapperScan("com.example.h2.mapper")
public class H2Application {
public static void main(String[] args) {
SpringApplication.run(H2Application.class, args);
}
}
编码
Entity 实体类
@Data
public class User {
private Long id;
private String name;
private Integer age;
private String email;
}
Mapper类
public interface UserMapper extends BaseMapper {
}
启动
@SpringBootTest
public class SampleTest {
@Autowired
private UserMapper userMapper;
@Test
public void testSelect() {
System.out.println(("----- selectAll method test ------"));
List userList = userMapper.selectList(null);
Assert.assertEquals(5, userList.size());
userList.forEach(System.out::println);
}
}
控制台输出
User(id=1, name=Jone, age=18, [email protected])
User(id=2, name=Jack, age=20, [email protected])
User(id=3, name=Tom, age=28, [email protected])
User(id=4, name=Sandy, age=21, [email protected])
User(id=5, name=Billie, age=24, [email protected])
使用Mock测试
上文用到了内嵌数据库可以完成单例测试,但是使用还是很繁琐,需要初始化数据库,另外如果是测试的函数中需要调用其他服务的接口,这时候就不是内嵌数据库可以解决的。因此我们可以使用另外一种方法,Mock测试。Mock是对于一些不容易构造/获取的对象,创建一个Mock对象来模拟对象的行为。Mock对象是虚构的,是可以构造任意你想要的数据。
在本章中,主要使用Mockito,一个强大的用于 Java 开发的模拟测试框架,而且使用简单。官方中文文档
Maven依赖
org.mockito
mockito-core
2.0.111-beta
编码
- 利用Mock对象记录行为
一旦mock对象被创建了,mock对象会记住所有的交互。你可以验证是否存在该操作
public void testMockito() {
//构建Mock对象
List mock = Mockito.mock(List.class);
//使用mock对象
mock.add("one");
//验证 mock对象是否进行过这些操作
Mockito.verify(mock).add("one");
//会抛出错误,因为没有进行过这个操作
// Mockito.verify(mock).remove("one");
}
- Mock最核心的功能,做测试桩(Stub)
测试桩Stub是什么呢?
写码的时候你会遇到一些外部依赖,比如在本机上写代码,可能会调用谷歌的API,来完成远程调用。而我在做测试的时候并不想真的发出这个请求,(贵,得不到想要的结果),因此我选择通过某种方式(Mockito)来进行模拟。Stub指的就是这种模拟,把服务端的依赖用本机来进行模拟
作者:CC stone
链接:https://www.zhihu.com/question/21017494/answer/604154516
@Test
public void testMockitoStub() {
// 你可以mock具体的类型,不仅只是接口
LinkedList mockedList = Mockito.mock(LinkedList.class);
//测试桩,这个算是埋点。当我们调用mockedList.get(0)的时候,会返回first
Mockito.when(mockedList.get(0)).thenReturn("first");
// 输出“first”
System.out.println(mockedList.get(0));
}
测试例子
不定期更新测试例子
主要写一下平时工作中会用到的测试方式,随着水平提高,应该会对单元测试有更深的理解注意: 测试用例统一使用的JUnit5
Junit5
的使用
JUnit 5
是一个项目名称(和版本),其 3 个主要模块关注不同的方面:JUnit Jupiter
、JUnit Platform
和 JUnit Vintage
。在单元测试中,我们通常只是使用到JUnit Jupiter
Junit Jupiter
模块
Junit Jupiter
模块用于编写单元测试,包含两个部分 JUnit Jupiter API
和 JUnit Jupiter Test Engine
-
JUnit Jupiter API
:使用 JUnit Jupiter API 创建单元测试来测试您的应用程序代码。使用该 API 的基本特性 — 注解、断言等 -
JUnit Jupiter Test Engine
:发现和执行JUnit Jupiter
单元测试,可将JUnit Jupiter Test Engine
看作单元测试与用于启动它们的工具(比如 IDE)之间的桥梁
JUnit Platform
模块
这个模块主要用于发现测试API和执行测试API。JUnit Platform
负责使用 IDE 和构建工具(比如 Gradle 和 Maven)发起测试发现流程。以前我们常用``@RunWith(SpringRunner.class)```在JUnit4,在
JUnit5我们使用
@RunWith(JUnitPlatform.class)`(对于一些支持JUnit5得IDE,不需要此注解了)
JUnit Vintage
模块
该模块主要是为了兼容JUnit4
。这个模块包含junit-vintage-engine
和 junit-jupiter-migration-support
组件
对 JUnit Platform
而言,JUnit Vintage
只是另一个测试框架,包含自己的 TestEngine
和 JUnit API
。
Junit5 注解
Junit5
的注解与Junit4
还是有不少区别的
注解 | 描述 |
---|---|
@Test |
表示测试方法,该注解没有任何属性,因为JUnit Jupiter 测试扩展有专门的注解操作 |
@BeforeEach |
表示被注解的方法应在当前类的每个@Test ,类似于JUnit 4的@Before |
@AfterEach |
表示被注解的方法应该在当前类的所有@Test ,类似于JUnit 4的@After |
@BeforeAll |
表示被注解的方法应该在当前类的所有@Test,类似于JUnit 4的@BeforeClass |
@AfterAll |
表示被注解的方法应该在当前类的所有@Test,类似于JUnit 4的@AfterClass |
@RunWith |
对于支持Junit5 的IDE,不需要此注解。对于未支持的需要@RunWith(JUnitPlatform.class) 使用 |
@DisplayName |
声明测试类或者测试方法的自定义显示名称 |
@Disabled |
声明JUnit 不允许此@Test 方法 |
Junit5
断言
如果断言失败,用例即结束。
JUnit Jupiter
提供了许多JUnit4
已有的断言方法,并增加了一些适合与Java 8 lambda
一起使用的断言方法。
由org.junit.jupiter.api.Assertions
类提供
更多例子,可以查看官方文档
@Test
void standardAssertions() {
Assertions.assertEquals(2, 2);
Assertions.assertEquals(4, 4, "The optional assertion message is now the last parameter.");
Assertions.assertTrue(2 == 2, () -> "Assertion messages can be lazily evaluated -- "
+ "to avoid constructing complex messages unnecessarily.");
}
@Test
void groupedAssertions() {
// In a grouped assertion all assertions are executed, and any
// failures will be reported together.
Assertions.assertAll("person",
() -> assertEquals("John", person.getFirstName()),
() -> assertEquals("Doe", person.getLastName())
);
}
Junit5
假设
如果假设失败,相关测试用例被忽略,但与假设同级别的收尾工作还要继续执行。
JUnit Jupiter
附带了JUnit4
提供的一些assumption
方法的子集,并增加了一些适合与Java 8 lambda
一起使用的方法。
由org.junit.jupiter.Asumptions
类提供
更多例子,可以查看官方文档
@Test
void testOnlyOnCiServer() {
Asumptions.assumeTrue("CI".equals(System.getenv("ENV")));
// remainder of test
}
@Test
void testOnlyOnDeveloperWorkstation() {
Asumptions.assumeTrue("DEV".equals(System.getenv("ENV")),
() -> "Aborting test: not on developer workstation");
// remainder of test
}
Junit5 参数化测试
有时候我们需要传值测试,在Junit5
中,支持我们传入参数进行测试。
数据库的初始化请看上文的内嵌数据库
@JdbcTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
public class ParameterTest {
@Autowired
private JdbcTemplate jdbcTemplate;
@ParameterizedTest
@ValueSource(longs = {1L,2L,3L,4L,5L})
public void parameterTest(long id) {
String sql = "select * from user where id=?";
RowMapper rowMapper=new BeanPropertyRowMapper(User.class);
List users = jdbcTemplate.query(sql, rowMapper,id);
System.out.println(users);
}
}
参数化测试需要用到@ParameterizedTest
和@ValueSource
Spring boot 单元测试
spring-boot-starter-test 测试包
spirng boot
对于测试,提供了一个spring-boot-starter-test包
org.springframework.boot
spring-boot-starter-test
在这个包中,包含了以下的库
-
Junit5
:单元测试Java
,目标是为JVM上的开发人员端测试创建最新的基础。官方文档 -
Spring
测试和Spring boot
测试:Spring Boot
应用程序的实用程序和集成测试支持 -
AssertJ
:一个流程的断言库 -
Hamcrest
:匹配器对象库 -
Mockito
:一个Java模拟框架 -
JSONassert
:JSON的断言库 -
JsonPath
:JSON的XPath
简单的Spring boot 单元测试
@SpringBootTest
public class Test {
@org.junit.jupiter.api.Test
public void test() {
System.out.println("Hello World");
}
}
- 使用的注解是的包是
org.junit.jupiter.api
这是JUnit5,而不是org.junit
。 - 由于使用JUnit5,所以
@RunWith(SpringRunner.class)
已经不再需要了
@SpringBootTest
工作原理与项目的启动类中@SpringBootApplication
差不多。@SpringBootTest
提供了webEnvironment
属性,默认是MOCK
,不会启动嵌入式服务器,所以不会起端口。
具体参数如下:
MOCK
(默认):不会启动嵌入式服务器。但是提供模拟网络环境,可以使用@AutoConfigureMockMvc
或者@AutoConfigureWebTestClient
测试Web应用程序接口。RANDOM_PROT
:启动嵌入式服务器,并且随机监听端口DEFINED_PORT
:启动嵌入式服务器,定义配置文件的端口或者默认端口8080NONE
:不提供任何模拟网络环境
spring boot 分片测试
当项目很庞大,每次启动耗时都很长的时候,就需要考虑只加载需要测试的配置和资源。**spring boot **提供了很多自动配置的注解,这些注解只会加载对应的资源信息,这会大大提高你的单元测试效率。
**spring boot **提供的注解如下,更多内容请查阅官方文档
-
@DataJdbcTest
:加载JdbcTemplateAutoConfiguration
,DataSourceAutoConfiguration
等配置。 -
@DataJpaTest
: 加载HibernateJpaAutoConfiguration
,DataSourceAutoConfiguration
-
@DataLdapTest
:加载LdapAutoConfiguration
-
@DataMongoTest
:加载Mongodb
配置 -
@DataNeo4jTest
:加载Neo4j
-
@DataRedisTest
:加载redis
-
@JdbcTest
:加载DataSource
-
@JooqTest
:加载Jooq
-
@JsonTest
: 加载Json
配置,GSON,Jackson
都支持 -
@RestClientTest
:加载RestTemplate
配置 -
@WebFluxTest
:加载WebFlut
配置 -
@WebMvcTest
:加载SpringMvc
配置
数据库测试,@JdbcTest
的使用
@JdbcTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
public class H2Test {
@Autowired
private DataSource dataSource;
@Test
public void h2Test() {
System.out.println(dataSource);
}
}
@JdbcTest
注解会自动加载以下类相关的配置
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration
org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration
org.springframework.boot.test.autoconfigure.jdbc.TestDatabaseAutoConfiguration
@AutoConfigureTestDatabase
有3种模式
-
ANY
: 测试数据源代替所有的自动配置数据源和手动定义的数据源 -
AUTO_CONFIGURED
:测试数据源仅代替所有自动配置的数据源 -
NONE
:不代替系统默认数据源,当你不想启动内嵌数据库的时候,可以选择这个模式
Spring MVC测试 @WebMvcTest
- 场景1
Controller类
@RestController
@RequestMapping("/user")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/info")
public User userInfo(@RequestParam long id) {
return userService.findById(id);
}
}
测试类
@WebMvcTest(UserController.class)
public class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
public void testMvc() throws Exception {
Mockito.when(userService.findById(Mockito.eq(1L))).thenReturn(buildUser());
/**
* mockMvc.perform 开始执行一个请求
* MockMvcRequestBuilders.get(xxx) 构建一个get方法的请求
* accept(MediaType.APPLICATION_JSON_UTF8_VALUE) header头信息,Accept:"application/json;charset=UTF-8"
* param 请求参数
* andExpect 添加执行完成后的断言
* andDo 返回结果处理器,可以添加一个对结果处理的Handler,例如MockMvcResultHandlers.print()
*/
mockMvc.perform(MockMvcRequestBuilders.get("/user/info")
.accept(MediaType.APPLICATION_JSON_UTF8_VALUE)
.param("id","1"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().json("{\"id\":1,\"name\":\"测试人员1\",\"age\":15,\"email\":\"[email protected]\"}"))
.andDo(MockMvcResultHandlers.print());
}
private User buildUser() {
return new User().setId(1).setName("测试人员1").setAge(15).setEmail("[email protected]");
}
@WebMvcTest(UserController.class)
是Spring boot
提供的单个片测试注解,value=UserController.class
相当于StandaloneMockMvcBuilder
测试方式(相对应的还有DefaultMockMvcBuilder
集成Web环境测试),独立构建UserController
的Web环境。
@MockBean
用在构建UserService
对象。因为UserService
需要查询数据库,有点麻烦。所以我就想用Mock
方式了,主要想测试Controller
层,就没必要关心其他层了。
- 场景2
当你想访问真正的Service层逻辑的时候,而不是用Mock构建Service层对象时。我们可以用MockMvcBuilders
来构建我们想要的MockMvc
。
@SpringBootTest
@ActiveProfiles("test")
class AccountControllerTest {
private MockMvc mockMvc;
@Autowired
protected WebApplicationContext wac;
@BeforeEach
@DisplayName("初始化MockMvc")
public void init() {
UserController bean = wac.getBean(UserController.class);
mockMvc = MockMvcBuilders.standaloneSetup(bean).build();
}
@Test
public void testMvc() throws Exception {
//测试代码同上
}
}
WebApplicationContext
是实现ApplicationContext
接口的子类。 它允许从相对于Web根目录的路径中加载配置文件完成初始化工作。从WebApplicationContext
中可以获取ServletContext
引用,整个Web应用上下文对象将作为属性放置在ServletContext
中,以便Web应用环境可以访问Spring上下文。
MockMvcBuilders.standaloneSetup(bean)
构建单个Controller
的MockMvc
,可以提高项目启动速度。
控制台输出结果
request 信息
MockHttpServletRequest:
HTTP Method = GET
Request URI = /user/info
Parameters = {id=[1]}
Headers = [Accept:"application/json;charset=UTF-8"]
Body =
Session Attrs = {}
response信息
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json;charset=UTF-8"]
Content type = application/json;charset=UTF-8
Body = {"id":1,"name":"测试人员1","age":15,"email":"[email protected]"}
Forwarded URL = null
Redirected URL = null
Cookies = []
Json
测试 @JsonTest
@JsonTest
public class MyJsonTest {
/*
* 相对应的,你也可以使用@JsonbTester,@GsonTester,@BasicJsonTester
*/
@Autowired
private JacksonTester jacksonTester;
@BeforeEach
public void init() {
ObjectMapper objectMapper = new ObjectMapper();
JacksonTester.initFields(jacksonTester,objectMapper);
}
@Test
public void t() throws IOException {
JsonContent jsonContent = jacksonTester.write(buildUser());
//断言Json串是一样
Assertions.assertThat(jsonContent).isEqualToJson("{\"id\":1,\"name\":\"测试人员1\",\"age\":15}");
//断言name属性的值是测试人员1
Assertions.assertThat(jsonContent).hasJsonPathStringValue("name","测试人员1");
//断言email属性是空的
Assertions.assertThat(jsonContent).hasEmptyJsonPathValue("email");
}
private User buildUser() {
return new User().setId(1).setName("测试人员1").setAge(15).setEmail("[email protected]");
}
}
@JsonTest
注解会自动配置Jackson的ObjectMapper
,所有@JsonComponent
bean和Jackson Modules
。
如果使用Jackson
,你想自定义ObjectMapper
,可以在@BeforeEach
的方法中使用JacksonTester.initFields
方法。
同样地Json
测试也有提供断言方法。可以用org.assertj.core.api.Assertions
类来断言JsonContent
对象
参考文章:
- Junit 5文章
- Junit 5官方文档
- Spring boot 官方文档
原文博客:Doi技术团队
链接地址:https://blog.doiduoyi.com/authors/1584445442095
初心:记录优秀的Doi技术团队学习经历