JUnit 5 初探

1. JUnit 和 JUnit 5

相信很多软件开发对于单元测试和Junit都不会感到陌生。JUnit 是由两位编程大师Kent Back 和 Erich Gamma 在1997年编写的Java开源单元测试框架,它通过大量的注解(Annotation)和约定(Convention) 运行和管理单元测试用例。
JUnit 的作者 Kent Back 曾经说过,软件开发如果没有单元测试就像人走在钢丝上,没有任何的保障。

Junit 5 是Junit框架的一次重大升级,充分利用了Java 8以及后续版本Java语言中大量新的特性,提升单元测试的编写效率和运行效率。在新框架中使用函数式和声明式编程风格在新框架中更容易编单元测试使用以及更具可读性。同时需要注意JUnit 5 需要Java 8 或者更高的版本运行,但支持低版本编译后的被测试的代码,但可能不是那么友好,强烈建议低于Java 8 的应用尽快迁移到Java 8 或者更高版本.

2. JUnit 5 架构

跟之前JUnit版本不同,JUnit 5 由几个不同模块组成,这几个模块同时也是JUnit 5的子项目。

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit 5 初探_第1张图片

2.1 JUnit Platform

JUnit Platform 主要负责在JVM上运行测试框架,它在JUnit和它的调用者之间定义了稳定且强大的接口,例如构建工具。JUnit Platform很容易让调用者集成JUnit用于发现或者执行单元测试用例。同时它还定义了TestEngine API 用于开发自定义测试构架同时运行在JUnit Platform上面。通过自定义实现TestEgnine API, 我们可以插入第三方测试组件到JUnit里面。

2.2 JUnit Jupiter

JUnit Jupiter 组件包含了用于编写JUnit 5 测试用例的新编程和扩展模型。相对于JUnit 4, JUnit 5提供了以下新的注解:

  • @TestFactory – 表示这是一个用于动态测试用例的测试工厂方法
  • @DisplayName – 为测试类或者测试方法定义一个显示名字让团队易于理解
  • @Nested – 表示这个类是嵌套的,非静态测试类
  • @Tag – 定义标签以便用例执行时过滤测试用例
  • @ExtendWith – 注册自定义扩展
  • @BeforeEach – 表示这个被注解方法在每一个测试用例执行前都会执行一次(同之前的 @Before),可用于初始化测试数据
  • @AfterEach – 表示这个被注解方法在每一个测试用例执行后都会执行一次(同之前的 @After),可用于清理上一次执行结果
  • @BeforeAll – 表示这个被注解方法在所有测试用例执行前都会执行一次(同之前的 @BeforeClass),相当于整个测试类的初始化。
  • @AfterAll – 表示这个被注解方法在所有测试用例执行后都会执行一次(同之前的@AfterClass),相当于整个测试类回收或者清理。
  • @Disable – 禁用一个测试类或者方法 (previously @Ignore)

2.3 JUnit Vintage

JUnit Vintage 主要为了支持JUnit 3/4的单元测试用例在JUnit 5上面运行,对于全新的项目或者JUnit 5 测试则无需引入。

3. JUnit 5 示例

3.1 JUnit 5 Maven 依赖

  • 将Junit 5 组件加入到pom.xml , 此处引入最新的版本。另外JUnit 5 Platform提供丰富的组件跟不同的工具平台集成,可按需引入。比如说junit-platform-console、junit-platform-reporting、junit-platform-launcher。

  	org.junit.jupiter
  	junit-jupiter
   	RELEASE
   	test
 

3.2 基础注解 (Basic Annotations)

  • @BeforeAll,@BeforeEach,@DisplayName,@Disabled,@AfterEach,@AfterAll
@BeforeAll
static void setup() {
	log.info("@BeforeAll -在此测试类里所有测试方法执行前执行一次");
}

@BeforeEach
void init() {
	log.info("@BeforeEach - 在此测试类里每一个测试方法执行前执行一次");
}

@DisplayName("正向测试用例1")
@Test
void testSingleSuccessTest() {
	log.info("Success");
}

@DisplayName("负向测试用例1")
@Test
void testSingleFailedTest() {
	log.info("failed");
}
	
@Test
@Disabled("Not implemented yet")
	void testShowSomething() {
}
	
@AfterEach
void tearDown() {
	log.info("@AfterEach - 在此测试类里每一个测试方法执行后执行一次");
}

@AfterAll
static void done() {
	log.info("@AfterAll - 在此测试类里所有测试方法执行后执行一次");
}
注意@BeforeAll和@AfterAll 需要定义为静态方法,否则无法通过编译。

执行结果和日志如下图:
JUnit 5 初探_第2张图片

3.3 断言(Assertions)

JUnit 5 从Java 8 引入大量的特性以提高测试编写效率,特别是lambda表达式。Assertions 在JUnit 5 中已经全部移至org.junit.jupiter.api目录下,所有的断言方法均为静态方法。使用lambda表达式其中一个作用是并行处理构造用例输出以节约时间和资源。

  • 示例1: 使用lambda表达式构建测试用例
@DisplayName("使用lambda表达式构建测试用例1")
@Test
void lambdaExpressions() {
	List numbers = Arrays.asList(1, 2, 3);
	assertTrue(numbers.stream()
	.mapToInt(Integer::intValue)
	.sum() == 6, () -> "List中数值之和等于6");
}
		

执行结果和日志如下图:
JUnit 5 初探_第3张图片

  • 示例2: 使用assertAll构建测试用例断言组合,若断言组合中任何一个断言失败将抛出MultipleFailuresError。
@DisplayName("使用assertAll将多个断言组合在一起")
@Test
void groupAssertions() {
	int[] numbers = {0, 1, 2, 3, 4};
	assertAll("numbers",
		() -> assertEquals(numbers[0], 1),
		() -> assertEquals(numbers[3], 3),
		() -> assertEquals(numbers[4], 1)
	);
}

执行结果和日志如下图:
JUnit 5 初探_第4张图片

3.4 假设(Assumptions)

Assumptions 在JUnit 5 中也全部移至org.junit.jupiter.api目录下,所有的断言方法均为静态方法。Assumptions 主要用于进入测试前的条件判断,一般用于判断外部条件是否满足,比如说单元测试的运行环境是否能满足单元测试运行。

  • 示例1: 检查当前操作系统为 Windows 要后继续执行
@DisplayName("使用Assumption构建测试用例正常执行")
@Test
public void trueAssumption() {
	assumeTrue(System.getProperty("os.name").contains("Windows"));
	assertEquals(5 + 2, 7);
}

执行结果和日志如下图:
JUnit 5 初探_第5张图片

  • 示例2: 检查当前操作系统为Linux要后继续执行, 若是非Linux则跳过执行并抛出TestAbortedException异常。
@DisplayName("使用Assumption构建测试用例不符合假设跳过执行")
@Test
public void trueAssumption() {
	assumeTrue(System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("linux"));
	assertEquals(5 + 2, 7);
}

执行结果和日志如下图:
JUnit 5 初探_第6张图片

3.5 异常测试(Exception Testing)

在JUnit 5中可以用assertThrows() 方法进行异常测试。

  • 示例1:通过异常信息来验证测试结果
@DisplayName("异常测试1:通过异常信息验证测试结果")
@Test
void shouldThrowException() {
	Throwable exception = assertThrows(UnsupportedOperationException.class, () -> {
		throw new UnsupportedOperationException("Not supported");
	});
	assertEquals("Not supported", exception.getMessage());
}

执行结果和日志如下图:
JUnit 5 初探_第7张图片

  • 示例2:通过异常类型来验证测试结果
@DisplayName("异常测试2:通过异常类型验证测试结果")
@Test
void assertThrowsException() {
	String str = null;
	assertThrows(IllegalArgumentException.class, () -> {
		Integer.valueOf(str);
	});
}

执行结果和日志如下图:
JUnit 5 初探_第8张图片

3.6 测试集合(Test Suites)

自定义测试集合在junit-platform模块,创建新的测试集合需要引入 junit-platform-suite-engine依赖,示例如下:


  	org.junit.platform
  	junit-platform-suite-engine
   	RELEASE
   	test

  • 示例1:一个简单的自定义测试集合
@Suite
@SelectPackages("me.wangyun.junit5")
@ExcludePackages("me.wangyun.suites")
@SuiteDisplayName("Junit 5 自定义测试集合")
public class AllUnitTestPackageSuites {
}

执行结果和日志如下图:
JUnit 5 初探_第9张图片
除了以上Pakcage级别的筛选,JUnit 5 还提供了以下Tag、Class级别的注解来将测试用例选择或者排除进入测试集合。

  • @SelectClasses
  • @SelectPackages
  • @IncludePackages
  • @ExcludePackages
  • @IncludeClassNamePatterns
  • @ExcludeClassNamePatterns
  • @IncludeTags
  • @ExcludeTags

3.6 动态测试用例(DynamicTest)

DynamicTest 是JUnit 5 的新特性。标准的测试用例在编写时用@Test注解并在编译期间指定,而动态测试用例是在运行期生成,这些测试用例由注解@TestFactory 的测试方法生成, 测试工厂方法不能为静态或者私有。动态测试用例不同于标准的测试用例,不支持标准测试用例的生命周期和回调,比如说@BeforeEach 和 @AfterEach 方法。

  • 示例一: 创建一个动态测试
@TestFactory
@DisplayName("测试创建动态测试集合1")
Collection dynamicTestsWithCollection() {
	return Arrays.asList(
		DynamicTest.dynamicTest("加法测试",
		() -> assertEquals(2, Math.addExact(1, 1))),
		DynamicTest.dynamicTest("乘法测试",
		() -> assertEquals(4, Math.multiplyExact(2, 2))));
}

执行结果和日志如下图:
JUnit 5 初探_第10张图片
JUnit 5 动态测试用例工厂除了Collection类型,还支持Iterable、Iterator、Stream等类型。动态测试用例可视为@ParameterizedTest的补充,@ParameterizedTest支持标准测试的完整生命周期(@BeforeEach 和 @AfterEach)。

4. 如何迁移JUnit 4 到 JUnit 5

尽管JUnit 5 Jupiter 编程和扩展模型不再支持JUnit 4的Rules和Runners特性,为了能让存量测试用例仍然能够在JUnit 5 平台上运行, JUnit 5提供了JUnit Vintage测试引擎运行JUnit 3和JUnit 4的测试用例。对于存量的单元测试用例迁移到JUnit 5有以下建议:

  • Annotations 迁移至 org.junit.jupiter.api 包。
  • Assertions 迁移至 org.junit.jupiter.api.Assertions。
  • Assumptions 迁移至 org.junit.jupiter.api.Assumptions。
  • 移除@Before 和 @After; 使用 @BeforeEach 和 @AfterEach 代替。
  • 移除@BeforeClass 和 @AfterClass; 使用 @BeforeAll 和 @AfterAll 代替。
  • 移除@Ignore; 使用 @Disabled 或者其它内置执行条件代替。
  • 移除@Category; 使用 @Tag 代替。
  • 移除@RunWith; 被@ExtendWith取代。
  • 移除@Rule 和 @ClassRule; 被 @ExtendWith and @RegisterExtension取代。
  • JUnit 5 Jupiter 中的断言和假设接受失败消息作为最后一个参数,而不是第一个参数。接受失败消息作为最后一个参数,而不是第一个参数。

5. 结论

JUnit 5 在架构上将单元测试运行平台和API进行解藕,将运行支持和用例编写的依赖分离。运行平台可以方便的集成IDE, 构建工具或者其它单元测试框架,让JUnit生态更加开放,更多的接口开放出交给用户自定义。试完一轮后,虽然目前没有发现一个特别强烈的理由将现有的JUnit 4的测试框架迁移到JUnit 5,但也看到新的框架更加开放,更灵活,运行效率更高。

6. 关于作者

微胖中年男码农隔壁老王,勿念。

你可能感兴趣的:(笔记,软件开发,junit,单元测试,java)