junit5

JUnit5在2017年就公布了,你还在用junit4吗?

什么是junit5

与以前的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 5

自从有了相似 JUnit 之类的测试框架,Java 单元测试畛域逐步成熟,开发人员对单元测试框架也有了更高的要求:更多的测试形式,更少的其余库的依赖。

因而,大家期待着一个更弱小的测试框架诞生,JUnit 作为Java测试畛域的领头羊,推出了 JUnit 5 这个版本,次要个性:

  • 提供全新的断言和测试注解,反对测试类内嵌
  • 更丰盛的测试形式:反对动静测试,反复测试,参数化测试等
  • 实现了模块化,让测试执行和测试发现等不同模块解耦,缩小依赖
  • 提供对 Java 8 的反对,如 Lambda 表达式,Sream API等。

根本注解

@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]类中的静态方法。

分组断言:

多个条件同时满足时才断言胜利

@Testvoid groupedAssertions() {    Person person = new Person();    Assertions.assertAll("person",                         () -> assertEquals("niu", person.getName()),                         () -> assertEquals(18, person.getAge())                        );}

异样断言:

Junit4时须要应用rule形式,junit5提供了assertThrows更优雅的异样断言

@Testvoid 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的次要变动之一,测试构造函数和办法当初都容许有参数。这带来了更大的灵活性,并为构造函数和办法启用依赖注入

  • TestInfo可获取测试信息
  • TestReporter能够向控制台输入信息
@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等数据

AssertJ

当定义好须要运行的测试方法后,下一步则是须要关注测试方法的细节,这就离不开断言和假如

断言:封装好了罕用判断逻辑,当不满足条件时,该测试用例会被认为测试失败

假如:与断言相似,当条件不满足时,测试会间接退出而不是断定为失败

因为不会影响到后续的测试用例,最罕用的还是断言

除了Junit5自带的断言,AssertJ是十分好用的一个断言工具,最大特点是提供了流式断言,与Java8应用办法十分相似

@Testvoid 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");}@Testvoid 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();}@Testvoid 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");}@Testvoid 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...

https://assertj.github.io/doc/