JUnit5在2017年就发布了,你还在用junit4吗?
与以前的JUnit版本不同,JUnit 5由三个不同子项目的多个不同模块组成。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform为在JVM上启动测试框架提供基础。它还定义了TestEngine API, 用来开发在平台上运行的测试框架。此外,平台提供了一个控制台启动器],用于从命令行启动平台,并为Gradle和Maven提供构建插件以[基于JUnit 4的Runner,用于在平台上运行任意TestEngine
。
JUnit Jupiter是在JUnit 5中编写测试和扩展的新型编程模型和[扩展模型][]的组合.Jupiter子项目提供了TestEngine
,用于在平台上运行基于Jupiter的测试。
JUnit Vintage提供TestEngine
,用于在平台上运行基于JUnit 3和JUnit 4的测试。
自从有了类似 JUnit 之类的测试框架,Java 单元测试领域逐渐成熟,开发人员对单元测试框架也有了更高的要求:更多的测试方式,更少的其他库的依赖。
因此,大家期待着一个更强大的测试框架诞生,JUnit 作为Java测试领域的领头羊,推出了 JUnit 5 这个版本,主要特性:
@Test: 表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
@ParameterizedTest: 表示方法是参数化测试
@RepeatedTest: 表示方法可重复执行
@DisplayName: 为测试类或者测试方法设置展示名称
@BeforeEach: 表示在每个单元测试之前执行
@AfterEach: 表示在每个单元测试之后执行
@BeforeAll: 表示在所有单元测试之前执行
@AfterAll: 表示在所有单元测试之后执行
@Tag: 表示单元测试类别,类似于JUnit4中的@Categories
@Disabled: 表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
@Timeout: 表示测试方法运行如果超过了指定时间将会返回错误
@ExtendWith: 为测试类或测试方法提供扩展类引用
常用注解格式:
class StandardTests { //与junit4的@beforeClass类似,每个测试类运行一次 @BeforeAll static void initAll() { } //与junit4中@before类似,每个测试用例都运行一次 @BeforeEach void init() { } @Test @DisplayName("成功测试") void succeedingTest() { } @Test @DisplayName("失败测试") void failingTest() { fail("a failing test"); } //禁用测试用例 @Test @Disabled("for demonstration purposes") void skippedTest() { // not executed } @Test void abortedTest() { assumeTrue("abc".contains("Z")); fail("test should have been aborted"); } //与@BeforeEach对应,每个测试类执行一次,一般用于恢复环境 @AfterEach void tearDown() { } //与@BeforeAll对应,每个测试类执行一次,一般用于恢复环境 @AfterAll static void tearDownAll() { } }
@DisplayName("显示名称测试") class DisplayNameDemo { @Test @DisplayName("我的 第一个 测试 用例") void testWithDisplayNameContainingSpaces() { } @Test @DisplayName("╯°□°)╯") void testWithDisplayNameContainingSpecialCharacters() { } @Test @DisplayName("") void testWithDisplayNameContainingEmoji() { } }
IDE运行测试结果显示:
**优点:**通过这种方式,可以在方法名是英文特别长或者很难用英文描述清楚的场景下,增加中文解释
JUnit Jupiter提供了许多JUnit4已有的断言方法,并增加了一些适合与Java 8 lambda一起使用的断言方法。所有JUnit Jupiter断言都是[org.junit.jupiter.Assertions]类中的静态方法。
分组断言:
多个条件同时满足时才断言成功
@Test void groupedAssertions() { Person person = new Person(); Assertions.assertAll("person", () -> assertEquals("niu", person.getName()), () -> assertEquals(18, person.getAge()) ); }
异常断言:
Junit4时需要使用rule方式,junit5提供了assertThrows更优雅的异常断言
@Test void exceptionTesting() { Throwable exception = assertThrows(IllegalArgumentException.class, () -> { throw new IllegalArgumentException("a message"); }); assertEquals("a message", exception.getMessage()); }
超时断言:
@Test @DisplayName("超时测试") public void timeoutTest() { Assertions.assertTimeout(Duration.ofMillis(100), () -> Thread.sleep(50)); }
通过标签把测试分组,在不同阶段执行不同的逻辑测试,比如划分为快速冒烟测试和执行慢但也重要的测试
@Test @Tag("fast") void testing_faster() { } @Test @Tag("slow") void testing_slow() { }
然后通过配置maven-surefire-plugin插件
<plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.0</version> <configuration> <properties> <includeTags>fast</includeTags> <excludeTages>slow</excludeTages> </properties> </configuration> </plugin>
当我们编写的类和代码逐渐增多,随之而来的需要测试的对应测试类也会越来越多。
为了解决测试类数量爆炸的问题,JUnit 5提供了@Nested 注解,能够以静态内部成员类的形式对测试用例类进行逻辑分组。
并且每个静态内部类都可以有自己的生命周期方法, 这些方法将按从外到内层次顺序执行。
此外,嵌套的类也可以用@DisplayName 标记,这样我们就可以使用正确的测试名称。下面看下简单的用法:
@DisplayName("A stack") class TestingAStackDemo { Stack<Object> stack; @Test @DisplayName("is instantiated with new Stack()") void isInstantiatedWithNew() { new Stack<>(); } @Nested @DisplayName("when new") class WhenNew { @BeforeEach void createNewStack() { stack = new Stack<>(); } @Test @DisplayName("is empty") void isEmpty() { assertTrue(stack.isEmpty()); } @Nested @DisplayName("after pushing an element") class AfterPushing { String anElement = "an element"; @BeforeEach void pushAnElement() { stack.push(anElement); } @Test @DisplayName("it is no longer empty") void isNotEmpty() { assertFalse(stack.isEmpty()); } } } }
junit没有限制嵌套层数,除非必要一般不建议使用超过3层,过于复杂的层次结构会增加开发者理解用例关系的难度
在之前的所有JUnit版本中,测试构造函数或方法都不允许有参数(至少不能使用标准的Runner实现)。作为JUnit Jupiter的主要变化之一,测试构造函数和方法现在都允许有参数。这带来了更大的灵活性,并为构造函数和方法启用依赖注入
@Test @DisplayName("test-first") @Tag("my-tag") void test1(TestInfo testInfo) { assertEquals("test-first", testInfo.getDisplayName()); assertTrue(testInfo.getTags().contains("my-tag")); } @Test @DisplayName("test-second") @Tag("my-tag") void test2(TestReporter testReporter) { testReporter.publishEntry("a key", "a value"); }
多次调用同一个测试用例
@RepeatedTest(10) @DisplayName("重复测试") public void testRepeated() { //... }
动态测试只需要编写一处代码,就能一次性对各种类型的输入和输出结果进行验证
@TestFactory @DisplayName("动态测试") Stream<DynamicTest> dynamicTests() { List<Person> persons = getAllPerson(); return persons.stream() .map(person -> DynamicTest.dynamicTest(person.getName() + "-test", () -> assertTrue(person.getName().contains("niu")))); }
通过时间来验证用例是否超时,一般要求单个单元测试不应该超过1秒
class TimeoutDemo { @BeforeEach @Timeout(5) void setUp() { // fails if execution time exceeds 5 seconds } @Test @Timeout(value = 1000, unit = TimeUnit.MILLISECONDS) void failsIfExecutionTimeExceeds1000Milliseconds() { // fails if execution time exceeds 1000 milliseconds //也可用这种方式 Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(1500)); } }
参数测试我觉得是最好用的特性,可以大量减少重复模板式代码,也是junit5最惊艳的提升,强烈推荐使用
@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
@NullSource: 表示为参数化测试提供一个null的入参
@EnumSource: 表示为参数化测试提供一个枚举入参
@CsvSource:表示读取CSV格式内容作为参数化测试入参
@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
@ArgumentsSource:指定一个自定义的,可重用的
ArgumentsProvider
。
看完用法描述,简直太喜欢了
一个顶三个基础测试用例
@ParameterizedTest @ValueSource(strings = {"one", "two", "three"}) @DisplayName("参数化测试1") public void parameterizedTest1(String string) { assertTrue(StringUtils.isNotBlank(string)); }
如果不是基础的类型,可以使用方法构造,只要返回值为Stream类型就可以,多个参数使用Arguments
实例流
@ParameterizedTest @MethodSource("method") @DisplayName("方法来源参数") public void testWithExplicitLocalMethodSource(String name) { Assertions.assertNotNull(name); } private static Stream<String> method() { return Stream.of("apple", "banana"); }
@CsvSource
允许您将参数列表表示为以逗号分隔的值(例如,字符串文字)
@ParameterizedTest @CsvSource({"steven,18", "jack,24"}) @DisplayName("参数化测试-csv格式") public void parameterizedTest3(String name, Integer age) { System.out.println("name:" + name + ",age:" + age); Assertions.assertNotNull(name); Assertions.assertTrue(age > 0); }
@CsvFileSource
使用classpath中的CSV文件,CSV文件中的每一行都会导致参数化测试的一次调用
这种就完全把测试数据与测试方法隔离,达到更好解耦效果
@ParameterizedTest @CsvFileSource(resources = "/persons.csv") //指定csv文件位置 @DisplayName("参数化测试-csv文件") public void parameterizedTest2(String name, Integer age) { System.out.println("name:" + name + ",age:" + age); Assertions.assertNotNull(name); Assertions.assertTrue(age > 0); }
其他方式不在赘述,如果还是满足不了需求,可以通过@ArgumentsSource自定义自己的数据来源,必须封装成去取JSON或者XMl等数据
当定义好需要运行的测试方法后,下一步则是需要关注测试方法的细节,这就离不开断言和假设
断言:封装好了常用判断逻辑,当不满足条件时,该测试用例会被认为测试失败
假设:与断言类似,当条件不满足时,测试会直接退出而不是判定为失败
因为不会影响到后续的测试用例,最常用的还是断言
除了Junit5自带的断言,AssertJ是非常好用的一个断言工具,最大特点是提供了流式断言,与Java8使用方法非常类似
@Test void testString() { // 断言null或为空字符串 assertThat("").isNullOrEmpty(); // 断言空字符串 assertThat("").isEmpty(); // 断言字符串相等 断言忽略大小写判断字符串相等 assertThat("niu").isEqualTo("niu").isEqualToIgnoringCase("NIu"); // 断言开始字符串 结束字符穿 字符串长度 assertThat("niu").startsWith("ni").endsWith("u").hasSize(3); // 断言包含字符串 不包含字符串 assertThat("niu").contains("iu").doesNotContain("love"); // 断言字符串只出现过一次 assertThat("niu").containsOnlyOnce("iu"); } @Test void testNumber() { // 断言相等 assertThat(42).isEqualTo(42); // 断言大于 大于等于 assertThat(42).isGreaterThan(38).isGreaterThanOrEqualTo(38); // 断言小于 小于等于 assertThat(42).isLessThan(58).isLessThanOrEqualTo(58); // 断言0 assertThat(0).isZero(); // 断言正数 非负数 assertThat(1).isPositive().isNotNegative(); // 断言负数 非正数 assertThat(-1).isNegative().isNotPositive(); } @Test void testCollection() { // 断言 列表是空的 assertThat(newArrayList()).isEmpty(); // 断言 列表的开始 结束元素 assertThat(newArrayList(1, 2, 3)).startsWith(1).endsWith(3); // 断言 列表包含元素 并且是排序的 assertThat(newArrayList(1, 2, 3)).contains(1, atIndex(0)).contains(2, atIndex(1)).contains(3) .isSorted(); // 断言 被包含与给定列表 assertThat(newArrayList(3, 1, 2)).isSubsetOf(newArrayList(1, 2, 3, 4)); // 断言 存在唯一元素 assertThat(newArrayList("a", "b", "c")).containsOnlyOnce("a"); } @Test void testMap() { Map<String, Object> foo = ImmutableMap.of("A", 1, "B", 2, "C", 3); // 断言 map 不为空 size assertThat(foo).isNotEmpty().hasSize(3); // 断言 map 包含元素 assertThat(foo).contains(entry("A", 1), entry("B", 2)); // 断言 map 包含key assertThat(foo).containsKeys("A", "B", "C"); // 断言 map 包含value assertThat(foo).containsValue(3); } // 其他断言,请自行探索......
想想如果没有使用AssertJ时我们是如何写断言的,是不是需要多个assert,很繁琐
AssertJ的断言代码清爽很多,流式断言充分利用了java8之后的匿名方法和stream类型的特点,很好的对Junit断言方法做了补充。
参考
https://junit.org/junit5/docs/current/user-guide/#overview
https://assertj.github.io/doc/
作者:你呀不牛
原文出处:https://www.cnblogs.com/niunotniu/p/14671941.html