单元测试
什么是单元测试
一个单元测试是一段代码,这段代码调用一个工作单元,并检验该工作单元的一个具体的最终结果。
如果关于这个最终结果的假设是错误的,单元测试就失败了。
一个单元测试的范围可以小到一个方法,大到一个类。
单元测试意义
很多人经常以 “时间紧,任务重” 或者 “单元测试没用” 为借口来拒绝编写单元测试。
但是 BUG 在软件的生命周期越早阶段发现,付出的代价越少。
单元测试可以让很多 BUG 在编码阶段就能够及时发现并解决,而不需要交给测试人员兜底,如果测试人员兜底失败,可能造成线上故障。
有了单元测试作保障,我们还可以放心对函数进行重构,如果重构代码导致单元测试运行失败,则说明重构的代码有问题。
长远来看,单元测试对编码的益处(如提高代码质量和避免 BUG)远比编写单元测试的投入所花费的代价要大的多。
单元测试标准
好的单元测试必须遵守 AIR 原则。
单元测试在线上运行时,感觉像空气(AIR)一样并不存在,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。
A: Automatic(自动化)
单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用 System.out 来进行人肉验证,必须使用 assert 来验证。I: Independent(独立性)
保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序。R: Repeatable(可重复)
单元测试是可以重复执行的,不能受到外界环境的影响。
单元测试结构
典型的单元测试可分为三个阶段,分别为准备、执行和验证。
- 准备阶段: 主要负责创建测试数据、构造mock 方法的返回值,准备环节的编码是单元测试最复杂的部分。需要注意的是 Mockito 库中以 when 开头的函数其实是在准备阶段。
- 执行阶段: 一般只是调用测试的函数,此部分代码通常较短。
- 验证阶段: 通常验证测试函数的执行的结果、 准备阶段 mock 函数的调用次数等是否符合预期。
单元测试工具
常用的 Java 单元测试有: JUnit、TestNG。
TestNG 受 JUnit 和 NUnit 的启发,功能相似,但是比 JUnit 更强大。TestNG 不只为单元测试而设计,其框架的设计目标是支持单元测试、公共能测试,端对端测试,集成测试等。
JUnit 具体用法比较简单,最新版本为JUnit5,如果想系统学习可官方使用指南,参考《JUnit 实战 (第 2 版)》, TestNG 和 JUnit 非常相似,如果想深入学习,首推 TestNG 官方文档。
主流的 Java mock 框架有: Mockito, JMockit, Easy Mock 。
Mockito 简洁易用,有 PowerMock 拓展,允许静态函数测试,社区强大,对结果的验证和异常处理非常简洁、灵活。缺点是框架本身不支持 static 和 private 函数的 mock。
JMockit 简单易用;可以 mock “一切”,包括 final 类, final/private/static 函数,而其他 mock 框架往往只支持其中一部分;缺点是社区支持不够活跃,3 个 contributers 介乎只有一个在干活,学习曲线比较陡峭。
Easy Mock 上手简单,文档清晰;同样的社区较小,导致更多人选择其它的 mock 框架。
还有很多其他配合单元测试的框架,如强大的构造随机 Java 对象的 Easy Random ,构造随机字符串的 Java Faker 等。
验证库有:Hamcrest,AssertJ。
构造单元测试数据的方式
单元测试的重要环节就是构造测试数据,单元测试构造测试数据往往非常耗时,这也是很多人不喜欢写单元测试的重要原因之一。如下为构造单侧数据的几种方式:
手动
手动构造单元测试数据,是指在测试类或者函数中直接声明测试数据(对象属性较多时,比较耗时):
@Test
void submitTest() {
// Setup
final UserInfo userInfo = new UserInfo();
final User user = new User();
user.setId(0L);
user.setOrgId(0L);
user.setCode("code");
user.setAccount("account");
}
半自动
半自动构造单元测试数据,是指使用插件自动填充所要构造对象的属性。采用 JSON 序列化和反序列化方式,
将构造的对象通过 JSON 序列化到 JSON 文件里,使用时反序列化为Java对象即可:
@Test
public void submitTest() {
// 构造测试数据
UserInfo userInfo = ResourceUtil.parseJson(UserInfo.class, "/data/userInfo.json");
log.info("构造的数据:{}", JSON.toJSONString(userInfo));
}
自动
半自动的方式构造单元测试数据效率仍然不够高,而且缺乏灵活性,比如需要构造随机属性的对象,需要构造不同属性的 N 个对象,就会造成编码复杂度陡增。
因此, Java Faker 和 Easy Random 应运而生。
java-faker
@Slf4j
public class FakeTest {
@Test
public void test() {
// 指定语言
Faker faker = new Faker(new Locale("zh-CN"));
// 姓名
String name = faker.name().fullName();
log.info(name);
String firstName = faker.name().firstName();
String lastName = faker.name().lastName();
log.info(lastName + firstName);
// 街道
String streetAddress = faker.address().streetAddress();
log.info(streetAddress);
// 颜色
Color color = faker.color();
log.info(color.name() + "-->" + color.hex());
// 大学
University university = faker.university();
log.info(university.name() + "-->" + university.prefix()+":"+university.suffix());
}
}
easy-random
easy-random 可以轻松构造复杂对象,支持定义对象中集合长度,字符串长度范围,生成集合等。
class EasyRandomTest {
private EasyRandom easyRandom;
@Test
void customRandomzierForFieldsShouldBeUsedToPopulateObjects() {
EasyRandomParameters parameters = new EasyRandomParameters()
.randomize(named("name").and(ofType(String.class)).and(inClass(Human.class)), randomizer);
easyRandom = new EasyRandom(parameters);
Person person = easyRandom.nextObject(Person.class);
assertThat(person).isNotNull();
assertThat(person.getName()).isEqualTo(FOO);
}
}