什么是单元测试

一个单元测试是一段代码,这段代码调用一个工作单元,并检验该工作单元的一个具体的最终结果。
如果关于这个最终结果的假设是错误的,单元测试就失败了。
一个单元测试的范围可以小到一个方法,大到一个类。

单元测试意义

很多人经常以 “时间紧,任务重” 或者 “单元测试没用” 为借口来拒绝编写单元测试。
但是 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 FakerEasy 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);  
	}
}