Junit5单元测试

1-前言

单元测试是软件开发过程中必不可少的一环,但是在平常开发中往往因为项目周期紧,工作量大而被忽略,这样往往导致软件问题层出不穷。线上出现不少问题其实在右单元测试的情况下就可以及时发现和处理,因此培养自己在日常开发中写单元测试的能力是很有必要的,无论是对自己编码能力的提高,还是项目质量的提升,都大有帮助。

2-认识Junit5

在java单元测试领域中,JunitTestNG占据着主要的市场,其中Junit有着就爱哦长的发展历史和不断演进的丰富功能,备受大多数Java开发者的青睐。

Junit5版本是Junit单元测试框架的一次重大升级,要完全使用Junit5的功能,就必须使用JDK8以上的环境。

与以前版本不同的是Junit5是由三个不同子项目的几个不同模块组成:

Junit 5 = Junit Platform + Junit Jupiter + Junit Vintage

  • Junit Platform:用于JVM上启动测试框架的基础服务,提供命令行,IDE和构建工具等方式执行测试的支持,并通过命令行定义TestEngine API;
  • Junit Jupiter:用于编写测试和扩展编程的扩展模型,然后通过插件在Junit、Gradle或Maven中来构建;
  • Junit Vintage:用于在Junit 5中兼容运行Junit3.x和Junit4.x的测试用例;

3-为什么需要Junit 5?

自从有了Junit之类的测试框架,Java单元测试领域逐渐成熟,开发人员对单元测试框架也有了更高的要求:

​ 如:更多的测试方法、更少的其他库依赖;

因此大家都期待着一个更强大的测试框架诞生,Junit作为Java测试领域的领头羊,推出了Junit 5这个版本;

主要特性有:

  • 提供全新的断言和测试注解,支持测试类内嵌,允许在断言中使用Lambda表达式
  • 更丰富的测试方法:支持动态测试,重复测试,参数化测试等
  • 实现了模块化,让测试执行和测试发现等不同模块解耦,减少依赖
  • 提供对Java8的支持,如Lambda表达式,Stream API等

4-如何使用Junit 5

  1. 首先需要添加Junit 5的依赖

    <dependencies>
    <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>${junit.jupiter.version}</version>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>${junit.version}</version>
    <scope>test</scope>
    </dependency>
    </dependencies>
  2. 定义测试方法

    import org.junit.jupiter.api.*(注解)描述
    @Test将方法标识为测试方法
    @RepeatedTest(重复测试
    @TestFactory方法是进行动态测试的工厂
    @BeforeEach在每次测试之前执行,它用于准备测试环境(如:读取输入数据,初始化类)
    @AfterEach在每次测试后执行,它用于清理测试环境(如:删除临时数据,恢复默认值),它还可以通过清理昂贵的内存结构来节省内存
    @BeforeAll在所有测试开始前执行一次,它用于执行耗时的活动,例如:连接到数据库;需要标记带有此批注的方法static才能与Junit一起使用
    @AfterAll在完成所有测试之后,执行一次,它用于执行清理活动,例如:与数据库断开连接;需要定义此注解的方法static以便与Junit一起使用
    @Nested使得可以嵌套内部测试类以强制执行一定的执行顺序,能够以静态内部类的形式对测试用例类进行逻辑分组
    @Tag(““)Junit 5中的测试可以通过标签过滤;例如:仅运行标签为“快速”的测试方法
    @ExtendWith可以让你注册一个或多个扩展点集成的扩展类
    @Disabled(@Disabled(“WhyDisabled”))指示应该禁用测试,当基础代码已更改且测试用例尚未适用时,这很有用;或者如果侧测试的执行时间太长而无法包括在内;最佳的做法是提供可选说明,说明为什么禁用测试
    @DisplayName(”“)<名称>,将由测试运行器显示,与方法名称相反,DisplayName可以包含空格
  3. 断言

    在断言API设计上,Junit 5进行显著的改进,并且充分利用Java 8的新特性,特别是Lambda表达式,最终提供了新的断言类

    org.junit.jupiter.api.Assertions;许多断言方法接收Lambda表达式参数,在断言消息使用Lambda表达式的一个优点就是踏实延迟计算的,如果消息构造开销很大,这样做一定程度上可以节省时间和资源。

    • assertAll:断言所有提供的可执行文件都不会抛出异常。若提供的标题(heading),其将包含在MultipleFailuresError的消息字符串中。
    • assertArrayEquals:断言期望的和实际的XX类型数组是相等的。若失败,将显示提供的失败消息。
    • assertDoesNotThrow:虽然从测试方法抛出的任何异常都会导致测试失败,但在某些用例中,显式断言测试方法中的给定代码块不会抛出异常会很有用。若提供的标题(heading),其将包含在MultipleFailuresError的消息字符串中。
    • assertEquals:断言预期和实际是相等的。如有必要,将从提供的messageSupplier中懒惰地检索失败消息。
    • assertFalse:断言提供的条件不是真。失败并显示提供的失败消息。
    • assertIterableEquals:断言预期和实际的迭代是完全相同的。类似于检查assertArrayEquals(Object [],Object [],String)中的完全相等,如果遇到两个迭代(包括期望和实际),则它们的迭代器必须以相同的顺序返回相等的元素。注意:这意味着迭代器不需要是同一类型。
    • assertNotNull:断言提供的条件不为null
    • assertNotSame:断言预期和实际不会引用同一个对象
    • assertNull:断言提供的实际为null
    • assertSame:断言预期和实际引用同一个对象
    • assertThrows:断言所提供的可执行代码块的执行会引发expectedType的异常并返回异常。如果没有抛出异常,或者抛出了不同类型的异常,则此方法将失败。如果不想对异常实例执行其他检查,只需忽略返回值。
    • assertTimeout:断言在超出给定超时之前,所提供的可执行代码块的执行完成。注意:可执行代码块将在与调用代码相同的线程中执行。因此,如果超过超时,则不会抢先中止执行可执行代码块。
    • assertTimeoutPreemptively:断言在超出给定超时之前,所提供的可执行代码块的执行完成。注意:可执行代码块将在与调用代码不同的线程中执行。此外,如果超过超时,则可抢占地执行可执行代码块。
    • assertTrue:断言提供的条件为true
    • fail:使用给定的失败消息以及根本原因进行测试失败。泛型返回类型V允许此方法直接用作单语句lambda表达式,从而避免需要实现具有显式返回值的代码块。 由于此方法在其return语句之前抛出AssertionFailedError,因此该方法实际上永远不会向其调用者返回值。
  4. 测试方法案例

    @DisplayName("我的第一个测试用例")
    public class MyFirstTestCaseTest {

    @BeforeAll
    public static void init() {
    System.out.println("初始化数据");
    }

    @AfterAll
    public static void cleanup() {
    System.out.println("清理数据");
    }

    @BeforeEach
    public void tearup() {
    System.out.println("当前测试方法开始");
    }

    @AfterEach
    public void tearDown() {
    System.out.println("当前测试方法结束");
    }

    @DisplayName("我的第一个测试")
    @Test
    void testFirstTest() {
    System.out.println("我的第一个测试开始测试");
    }

    @DisplayName("我的第二个测试")
    @Test
    void testSecondTest() {
    System.out.println("我的第二个测试开始测试");
    }
    }

    测试结果如下:

    第一个测试

    禁用测试方法:@Disabled

    @DisplayName("我的第三个测试")
    @Disabled
    @Test
    void testThirdTest() {
    System.out.println("我的第三个测试开始测试");
    }

    测试结果如下:

@Disabled 也可以使用在类上,用于标记类下所有的测试方法不被执行,一般使用对多个测试类组合测试的时候。

内嵌测试类:@Nested

当我们编写的类和代码逐渐增多,随之而来的需要测试的对应测试类也会越来越多。为了解决测试类数量爆炸的问题,JUnit 5提供了@Nested 注解,能够以静态内部成员类的形式对测试用例类进行逻辑分组。 并且每个静态内部类都可以有自己的生命周期方法, 这些方法将按从外到内层次顺序执行。 此外,嵌套的类也可以用@DisplayName 标记,这样我们就可以使用正确的测试名称。

@DisplayName("内嵌测试类")
public class NestUnitTest {
@BeforeEach
void init() {
System.out.println("测试方法执行前准备");
}

@Nested
@DisplayName("第一个内嵌测试类")
class FirstNestTest {
@Test
void test() {
System.out.println("第一个内嵌测试类执行测试");
}
}

@Nested
@DisplayName("第二个内嵌测试类")
class SecondNestTest {
@Test
void test() {
System.out.println("第二个内嵌测试类执行测试");
}
}
}

测试结果如下:

重复性测试:@RepeatedTest
在 JUnit 5 里新增了对测试方法设置运行次数的支持,允许让测试方法进行重复运行。当要运行一个测试方法 N次时,可以使用 @RepeatedTest 标记它

@DisplayName("重复测试")
@RepeatedTest(value = 3)
public void i_am_a_repeated_test() {
System.out.println("执行测试");
}

测试结果如下:

  1. 断言案例

    class AssertionsDemo {
    //定义一个person对象,Person类里面有两个参数lastName,firstName
    static Person person = new Person();

    /**
    * 使用 @BeforeAll注解在所有测试方法执行前执行person对象的赋值
    */
    @BeforeAll
    static void initPerson(){
    person.setFirstName("John");
    person.setLastName("Doe");
    }

    /**
    * assertEquals比较两个值是否相同
    * assertTrue 判断括号里面的参数是否为true
    */
    @Test
    void standardAssertions() {
    assertEquals(2, 2);
    //当不相等时,会打印出第三个参数,下面的所有的此类型的参数都是这种作用
    assertEquals(4, 5, "The optional assertion message is now the last parameter.");
    assertTrue('a' < 'b', "Assertion messages can be lazily evaluated -- "
    + "to avoid constructing complex messages unnecessarily.");
    }

    /**
    * assertAll()方法用于将多个测试语句放在一个组中执行
    * 组中若有一个测试语句不通过,则这个组将会一起报错.
    * 方法中第一个参数:组名称
    * 方法中第二个参数:组测试语句
    */
    @Test
    void groupedAssertions() {
    assertAll("person",
    () -> assertEquals("John", person.getFirstName()),
    () -> assertEquals("Doe", person.getLastName())
    );
    }

    /**
    * assertAll()方法也可以嵌套多个assertAll()方法
    * 其中嵌套的多个测试组,这些组只会打印出这个组和父组的错误,对其他的组没有影响
    */
    @Test
    void dependentAssertions() {
    assertAll("properties",
    //第一个测试组
    () -> {
    String firstName = person.getFirstName();
    assertNotNull(firstName);

    assertAll("first name",
    () -> assertTrue(firstName.startsWith("J")),
    () -> assertTrue(firstName.endsWith("n"))
    );
    },
    //第二个测试组
    () -> {
    String lastName = person.getLastName();
    assertNotNull(lastName);

    assertAll("last name",
    () -> assertTrue(lastName.startsWith("D")),
    () -> assertTrue(lastName.endsWith("e"))
    );
    }
    );
    }

    /**
    * assertThrows()可以用来判断lambda表达式中的代码抛出的异常
    * 比如下面案例就是测试了抛出异常的信息是否相同
    * 参数:
    * 1:异常类声明
    * 2:测试代码Lambda表达式
    */
    @Test
    void exceptionTesting() {
    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
    try {
    //这里只是简单的做个测试,当然1/0不该抛IllegalArgumentException异常 ,只是简单的测试一下
    int s = 1/0;
    }catch (Exception e){
    throw new IllegalArgumentException("a message");
    }
    });
    assertEquals("a message", exception.getMessage());
    }

    /**
    * assertTimeout()对方法执行时间进行测试
    * 这里要借助java.time.Duration中的方法结合实现
    * 实例中执行的代码部分必须在2分钟之内执行完毕,否则测试不通过
    */
    @Test
    void timeoutNotExceeded() {
    assertTimeout(ofMinutes(2), () -> {
    //执行的代码部分
    });
    }

    /**
    * assertTimeout()还可以接受一个返回值(泛型 T)
    * 被测试代码如果通过测试并返回一个值,这个值被assertTimeout()方法返回
    */
    @Test
    void timeoutNotExceededWithResult() {
    String actualResult = assertTimeout(ofMinutes(2), () -> {
    return "a result";
    });
    assertEquals("a result", actualResult);
    }

    /**
    * assertTimeout()毫秒案例
    */
    @Test
    void timeoutExceeded() {
    assertTimeout(ofMillis(10), () -> {
    Thread.sleep(100);
    });
    }
    }

6-Junit参数化测试

  • @ValueSource——最简单的数据参数源,通过注解可以直接指定携带的运行参数,支持 Java 的八大基本类型和字符串,Class,使用时赋值给注解上对应类型属性,以数组方式传递。

    public class ValueSourcesExampleTest {

    @ParameterizedTest
    @ValueSource(ints = {2, 4, 8})
    void testNumberShouldBeEven(int num) {
    assertEquals(0, num % 2);
    }

    @ParameterizedTest
    @ValueSource(strings = {"Radar", "Rotor", "Tenet", "Madam", "Racecar"})
    void testStringShouldBePalindrome(String word) {
    assertEquals(isPalindrome(word), true);
    }

    @ParameterizedTest
    @ValueSource(doubles = {2.D, 4.D, 8.D})
    void testDoubleNumberBeEven(double num) {
    assertEquals(0, num % 2);
    }

    boolean isPalindrome(String word) {
    return word.toLowerCase().equals(new StringBuffer(word.toLowerCase()).reverse().toString());
    }
    }

    输出如下:

    [INFO] -------------------------------------------------------
    [INFO] T E S T S
    [INFO] -------------------------------------------------------
    [INFO] Running qiucao.learning.ParaTest
    [INFO] Tests run: 11, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.155 s - in qiucao.learning.ParaTest
    [INFO]
    [INFO] Results:
    [INFO]
    [INFO] Tests run: 11, Failures: 0, Errors: 0, Skipped: 0
  • @EnumSource——枚举参数源,允许我们通过将参数值由给定Enum枚举类型传入。并可以通过制定约束条件或正则匹配来筛选传入参数。

    public class EnumSourcesExampleTest {

    @ParameterizedTest(name = "[{index}] TimeUnit: {arguments}")
    @EnumSource(TimeUnit.class)
    void testTimeUnitMinimumNanos(TimeUnit unit) {
    assertTrue(unit.toMillis(2000000L) > 1);
    }

    @ParameterizedTest
    @EnumSource(value = TimeUnit.class, names = {"SECONDS", "MINUTES"})
    void testTimeUnitJustSecondsAndMinutes(TimeUnit unit) {
    assertTrue(EnumSet.of(TimeUnit.SECONDS, TimeUnit.MINUTES).contains(unit));
    assertFalse(EnumSet
    .of(TimeUnit.DAYS, TimeUnit.HOURS, TimeUnit.MILLISECONDS, TimeUnit.NANOSECONDS,
    TimeUnit.MICROSECONDS).contains(unit));
    }

    @ParameterizedTest
    @EnumSource(value = TimeUnit.class, mode = Mode.EXCLUDE, names = {"SECONDS", "MINUTES"})
    void testTimeUnitExcludingSecondsAndMinutes(TimeUnit unit) {
    assertFalse(EnumSet.of(TimeUnit.SECONDS, TimeUnit.MINUTES).contains(unit));
    assertTrue(EnumSet
    .of(TimeUnit.DAYS, TimeUnit.HOURS, TimeUnit.MILLISECONDS, TimeUnit.NANOSECONDS,
    TimeUnit.MICROSECONDS).contains(unit));
    }

    @ParameterizedTest
    @EnumSource(value = TimeUnit.class, mode = Mode.MATCH_ALL, names = ".*SECONDS")
    void testTimeUnitIncludingAllTypesOfSecond(TimeUnit unit) {
    assertFalse(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS, TimeUnit.MINUTES).contains(unit));
    assertTrue(EnumSet
    .of(TimeUnit.SECONDS, TimeUnit.MILLISECONDS, TimeUnit.NANOSECONDS,
    TimeUnit.MICROSECONDS).contains(unit));
    }

    }

    输出如下:

    [INFO] -------------------------------------------------------
    [INFO] T E S T S
    [INFO] -------------------------------------------------------
    [INFO] Running qiucao.learning.ParaTest
    [INFO] Tests run: 18, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.206 s - in qiucao.learning.ParaTest
    [INFO]
    [INFO] Results:
    [INFO]
    [INFO] Tests run: 18, Failures: 0, Errors: 0, Skipped: 0
  • @MethodSource——通过其他的Java方法函数来作为参数源。引用的方法返回值必须是Stream, Iterator 或者Iterable,指定一个返回的 Stream / Array / 可迭代对象 的方法作为数据源。 需要注意的是该方法必须是静态的,并且不能接受任何参数。

    public class MethodSourceExampleTest {
    @ParameterizedTest
    @MethodSource("stringGenerator")
    void shouldNotBeNullString(String arg){
    assertNotNull(arg);
    }

    @ParameterizedTest
    @MethodSource("intGenerator")
    void shouldBeNumberWithinRange(int arg){
    assertAll(
    () -> assertTrue(arg > 0),
    () -> assertTrue(arg <= 10)
    );
    }

    @ParameterizedTest(name = "[{index}] user with id: {0} and name: {1}")
    @MethodSource("userGenerator")
    void shouldUserWithIdAndName(long id, String name){
    assertNotNull(id);
    assertNotNull(name);
    }

    static Stream<String> stringGenerator(){
    return Stream.of("hello", "world", "let's", "test");
    }

    static IntStream intGenerator() {
    return IntStream.range(1,10);
    }

    static Stream<Arguments> userGenerator(){
    return Stream.of(Arguments.of(1L, "Sally"), Arguments.of(2L, "Terry"), Arguments.of(3L, "Fred"));
    }
    }

    输出如下:

    [INFO] -------------------------------------------------------
    [INFO] T E S T S
    [INFO] -------------------------------------------------------
    [INFO] Running qiucao.learning.ParaTest
    [INFO] Tests run: 16, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.191 s - in qiucao.learning.ParaTest
    [INFO]
    [INFO] Results:
    [INFO]
    [INFO] Tests run: 16, Failures: 0, Errors: 0, Skipped: 0
  • @ArgumentsSource——通过实现 ArgumentsProvider 接口的参数类来作为数据源,重写它的 provideArguments 方法可以返回自定义类型的 Stream,作为测试方法所需要的数据使用。

    public class ArgumentsSourceExampleTest {

    @ParameterizedTest
    @ArgumentsSource(CustomArgumentsGenerator.class)
    void testGeneratedArguments(double number) throws Exception {
    assertFalse(number == 0.D);
    assertTrue(number > 0);
    assertTrue(number < 1);
    }

    static class CustomArgumentsGenerator implements ArgumentsProvider {

    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
    return Stream.of(Math.random(), Math.random(), Math.random(), Math.random(), Math.random())
    .map(Arguments::of);
    }
    }
    }
  • @CsvSource——通过 @CsvSource 可以注入指定 CSV 格式 (comma-separated-values) 的一组数据,用每个逗号分隔的值来匹配一个测试方法对应的参数。

    public class CsvSourceExampleTest {

    Map<Long, String> idToUsername = new HashMap<>();

    {
    idToUsername.put(1L, "Selma");
    idToUsername.put(2L, "Lisa");
    idToUsername.put(3L, "Tim");
    }

    @ParameterizedTest
    @CsvSource({"1,Selma", "2,Lisa", "3,Tim"})
    void testUsersFromCsv(long id, String name) {
    assertTrue(idToUsername.containsKey(id));
    assertTrue(idToUsername.get(id).equals(name));
    }
    }

    输出如下:

    [INFO] -------------------------------------------------------
    [INFO] T E S T S
    [INFO] -------------------------------------------------------
    [INFO] Running qiucao.learning.ParaTest
    [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.164 s - in qiucao.learning.ParaTest
    [INFO]
    [INFO] Results:
    [INFO]
    [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
  • @CsvFileSource——除了使用csv参数源,这里也支持使用csv文件作为参数源

    假设users.csv 文件包含如下csv格式的数据:

    1,Selma
    2,Lisa
    3,Tim

    代码如下:

    public class CsvFileSourceExampleTest {

    Map<Long, String> idToUsername = new HashMap<>();

    {
    idToUsername.put(1L, "Selma");
    idToUsername.put(2L, "Lisa");
    idToUsername.put(3L, "Tim");
    }

    @ParameterizedTest
    @CsvFileSource(resources = "/users.csv")
    void testUsersFromCsv(long id, String name) {
    assertTrue(idToUsername.containsKey(id));
    assertTrue(idToUsername.get(id).equals(name));
    }
    }

    输出如下:

    [INFO] -------------------------------------------------------
    [INFO] T E S T S
    [INFO] -------------------------------------------------------
    [INFO] Running qiucao.learning.ParaTest
    [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.199 s - in qiucao.learning.ParaTest
    [INFO]
    [INFO] Results:
    [INFO]
    [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0