没写单测出BUG,该学一下Junit5了

huiyugan
发布于 2022-11-9 15:50
浏览
0收藏

什么是Junit5

​Junit​​​是Java语言中的一个流行测试框架,是由Kent Beck和Erich Gamma开发的。它的第一个版本于1997年发布。由于其易用性,它成为Java社区中最流行的测试框架之一。它是一个轻量级测试框架,允许Java开发人员用Java语言编写单元测试用例。最新发布的版本是5.8.2,被称为​​JUnit5​​。


JUnit 5由许多不同的模块组成。主要包括以下三个子模块:


  • Junit Platform
  • Junit Jupiter
  • Junit Vintage


以上三个模块构成了Junit5的核心功能。

Junit 5的架构

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

Junit5包含以下三部分核心组件:

Junit Platform

该模块提供了在JVM上启动测试框架的核心基础功能,充当JUnit与其客户端(如构建工具[​​Maven​​​、​​Gradle​​​]和IDE[​​Eclipse​​​、​​IntelliJ​​​])之间的接口。它引入了​​Launcher​​(启动器)的概念,外部工具可以使用它来发现、过滤和执行测试用例。


它还提供了TestEngine API,用于开发在JUnit上运行的测试框架;使用TestEngine API,第三方测试库(如Spock、Cucumber和FitNesse)可以直接j集成它们的自定义

TestEngine。


Junit Jupiter

该模块为在Junit 5中编写测试和扩展提供了一个新的编程模型和扩展模型。


它有一套全新的注解来编写Junit5中的测试用例,其中包括​​@BeforeEach​​​、​​@AfterEach​​​、​​@AfterAll​​​、​​@BeforeAll​​​等。可以理解为是对​​Junit Platform​​的TestEngine API的实现,以便Junit5测试可以运行。

Junit Vintage

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

​Vintage​​从字面意思理解是“古老的,经典的”。


该模块就是为了对Junit4和JUnit3编写的测试用例提供支持。因此,Junit5具备向后兼容能力。

快速入门

在我们开始编写Junit5测试之前,需要先具备以下条件。


Java 8+


Junit 5要求JDK版本最低是Java 8,所以我们需要先安装Java 8或更高版本的JDK。


IDE


我们肯定不能直接在记事本里编写代码,所以需要使用顺手的IDE,我因为习惯于使用IntelliJ IDEA,所以接下来的示例都是在IDEA中进行。


如果你更喜欢使用Eclipse,可以使用Eclipse Oxygen版本或者更高级的版本。


因为在Junit5的学习过程中不会用到特别多的Jar包依赖,所以这里先不使用Maven或Gradle构建工具。


接下来我们开始在IDEA中编写测试代码。


首先,在IDE中创建一个Java项目,JDK选择1.8版本。

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

然后一直点击Next,给我们的工程起一个名字,就叫Junit5吧。

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

接下来,在我们的项目中建一个test文件夹,并新建一个测试类FirstJunit5Test.java

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

现在就可以编写第一个测试用例啦。

package test;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.fail;

/**
 * @author 小黑说Java
 * @ClassName FirstJunit5Test
 * @Description
 * @date 2022/1/6
 **/
public class FirstJunit5Test {

    @Test
    public void test(){
        fail("还没有实现的测试用例");
    }
}

正常情况下,你在写这段代码时,会编译失败,因为我们的工程中现在还没有添加Junit5的Jar包。

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

通过代码提示,将Junit5的Jar包添加到classpath就可以了。


在我们上面的代码中,有一个test()方法,该方法上有一个@Test注解,表示这是一个测试方法,我们在这个方法中编写代码进行测试。


最后直接右键运行该测试方法。测试用例失败,如下面的图所示。它给出“​​AssertionFailedError:还没有实现的测试用例​​”。


没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区


这是因为在test()方法中,是使用​​fail("还没有实现的测试用例")​​断言。该断言未通过测试用例。

@Test注解

该注解是我们在编写测试用例时最常使用的一个注解。


接下来,我们先定义一个具有功能的类,然后通过测试用例来对功能进行不同场景的测试。

package test;

/**
 * @author 小黑说Java
 * @ClassName OddEven
 **/
public class OddEven {
    /**
     * 判断一个数是否为偶数
     */
    public boolean isNumberEven(int number){
        return number % 2 == 0;
    }

}

很简单的一个功能,在OddEven类中有一个isEvenNumber()方法,用来判断一个数字是奇数还是偶数。如果为偶数返回true,反之返回false。


接下来我们编写测试代码。 为了测试isEvenNumber()方法,我们需要编写覆盖其功能的测试用例。


  • 传入偶数,它应该返回true;
  • 传入奇数,应该返回false。


为了将测试类中创建的方法识别为测试方法,我们需要使用@Test注释对其进行标记。让我们来看看测试类和测试方法:

package test;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * @author 小黑说Java
 * @ClassName OddEvenTest
 * @Description
 * @date 2022/1/6
 **/
public class OddEvenTest {
    @Test
    void evenNumberTrue(){
        OddEven oddEven = new OddEven();
        assertTrue(oddEven.isNumberEven(10));
    }

    @Test
    void oddNumberFale(){
        OddEven oddEven = new OddEven();
        assertFalse(oddEven.isNumberEven(11));
    }
}

在以上代码中,我们使用到了Junit5的断言assertTrue()和assertFasle();


assertTrue()方法接受布尔值并确保该值为true。如果传递的是false,则测试用例将失败。


assertFalse()方法接受布尔值并确保该值为false。如果传递的值为true,则测试用例将失败。


运行上面的测试用例,会得到如下结果,表示测试用例通过。

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

什么是断言?

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

如果你不理解断言的字面意思,可以看一下翻译,asserts表示明确肯定。


没错,小黑哥不光讲技术,还教英语~


在JUnit5中断言的作用就是帮助我们用测试用例的实际输出验证预期输出。


简而言之,断言是我们在测试中用来验证预期行为的静态方法。


所有JUnit5的断言都在​​org.juit.jupiter.Assertions​​类中。这些方法支持Java8 lambda表达式,并被大量重载以支持不同类型,如基本数据类型、对象、stream、数组等。

断言方法

断言方法

作用

assertNull()

断言实际输出为null.

assertNotNull()

断言实际输出不为null.

fail()

让测试用例不通过

assertSame()

断言期望值和实际值是用一个对象

assertNotSame()

断言期望值和实际值不是用一个对象

assertTrue()

断言实际值为true

assertFalse()

断言实际值为false

assertEquals()

断言期望值和实际值相等

assertNotEquals()

断言期望值和实际值不相等

assertArrayEquals()

断言期望数组和实际数组相等

assertIterableEquals()

断言期望可迭代容器和实际可迭代容器相等

assertThrows()

断言可执行代码中会抛出期望的异常类型

assertAll()

断言一组中的多个

assertTimeout()

断言一段可执行代码的会在指定时间执行结束

assertTimeoutPreemptively()

断言可执行代码如果超过指定时间会被抢占中止

在上一节内容中我们初步使用了fail(),assertTrue(), assertFalse(),接下来我们重点介绍一下其他几个断言。

assertNull()

该断言方法帮助我们验证特定对象是否为空。


  • 如果实际值为空,则测试用例将通过
  • 如果实际值不为空,则测试用例将失败


​assertNull()​​有三种重载方法,如下所述:

public static void assertNull(Object actual)

public static void assertNull(Object actual, String message)

public static void assertNull(Object actual, Supplier<String> messageSupplier)
  • AssertNull(Object Actual)-它断言实际值是否为空。
  • AssertNull(Object Actual,String Message)-它断言实际值是否为空。在这种情况下,如果实际值不为空,则测试用例将失败,并显示一条提供的消息。
  • AssertNull(Object Actual,SuppliermessageSupplier)-它断言实际值是否为空。在这种情况下,如果实际值不为空,则测试用例将失败,并通过供应商功能提供一条消息。使用Supplier函数的主要优点是,只有在测试用例失败时,它才懒惰地计算为字符串。


接下来我们编写一个代码案例。假设我们现在有一个字符串工具类StringUtils,该类中提供一个reverse(String)方法,可实现字符串反转功能。

package com.heiz123.junit5;

/**
 * @author 小黑说Java
 * @ClassName StringUtils
 * @Description
 * @date 2022/1/6
 **/

public class StringUtils {

    public static String reverse(String input){
        if (input == null) {
            return null;
        }

        if (input.length() == 0) {
            return "";
        }

        char[] charArray = input.toCharArray();
        int start = 0;
        int end = input.length() - 1;

        while (start < end) {
            char temp = charArray[start];
            charArray[start] = charArray[end];
            charArray[end] = temp;
            start++;
            end--;
        }

        return new String(charArray);
    }
}

我们使用assertNull()断言来对reverse()方法编写如下测试用例。


  • 如果我们以“ABC”的形式提供输入字符串,它将返回“CBA”
  • 如果我们提供的输入字符串为null,则返回null
  • 如果我们将输入字符串作为“”提供,它将返回“”字符串


编写一个测试类StringUtilsTest,代码如下:

package test;


import static org.junit.jupiter.api.Assertions.*;

import java.util.function.Supplier;

import com.heiz123.junit5.StringUtils;
import org.junit.jupiter.api.Test;

/**
 * @author 小黑说Java
 * @ClassName StringUtilsTest
 * @Description
 * @date 2022/1/6
 **/

class StringUtilsTest {

    @Test
    void nullStringRevered(){
        String actual = StringUtils.reverse((null));
        assertNull(actual);
    }

    @Test
    void emptyStringReversed(){
        String actual = StringUtils.reverse((""));
        String message = "Actual String should be null !!! ";
        assertNull(actual, message);
    }

    @Test
    void NonNullStringReversed(){
        String actual = StringUtils.reverse(("ABC"));
        Supplier<String> messageSupplier = () -> "Actual String should be null !!! ";
        // assertNull使用Java 8的MessageSupplier
        assertNull(actual, messageSupplier);
    }

}

执行测试用例结果我们发现,只有nullStringRevered()测试用例通过。

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

如果你多次执行测试用例会发现执行顺序并不固定。


  • 在StringUtilsTest类中有3个@Test方法: nullStringRevered():当向reverse()方法传入null时,则返回null。因此,assertNull()会断言实际返回的值为空。它通过了Junit测试用例。
  • emptyStringReversed():当向reverse()方法传入""时,则返回""。这里返回值为空字符串,不为空。因此,它无法通过Junit测试用例。在此测试用例中,我们使用重载的assertNull()方法,该方法将字符串消息作为第二个参数。因为这个测试用例不满足断言条件,所以它失败,并给出“​​AssertionFailedError: Actual String should be null !!! ==> Expected :null Actual :​​”。
  • NonNullStringReversed:当向reverse()方法传入"ABC"时,返回"CBA"。这里,返回值不为空。因此,它无法通过Junit测试用例。在此测试用例中,使用重载的​​assertNull()​​​方法,该方法将​​Supplier<String>messageSupplier​​​作为第二个参数。因为此测试用例不满足断言条件,所以它失败,并抛出“​​AssertionFailedError: Actual String should be null !!! ==> Expected :null Actual :CBA​​”。

assertThrows()

该断言方法有助于断言用于验证一段代码中是否抛出期望的异常。


  • 如果没有引发异常,或者引发了不同类型的异常,则此方法将失败;
  • 它遵循继承层次结构,因此如果期望的类型是Exception,而实际是RuntimeException,则断言也会通过。


assertThrow()也有三种有用的重载方法:

public static <T extends Throwable> T assertThrows(Class<T> expectedType, Executable executable)
  
public static <T extends Throwable> T assertThrows(Class<T> expectedType, Executable executable, String message)
  
public static <T extends Throwable> T assertThrows(Class<T> expectedType, Executable executable, Supplier<String> messageSupplier)

我们通过以下测试代码来看一下这个断言:

package test;

import org.junit.jupiter.api.Test;

import java.io.IOException;

import static org.junit.jupiter.api.Assertions.assertThrows;

/**
 * @author 小黑说Java
 * @ClassName AssertThrowTest
 * @Description
 * @date 2022/1/6
 **/
public class AssertThrowTest {

    @Test
    public void testAssertThrows(){
        assertThrows(ArithmeticException.class, () -> divide(1, 0));
    }

    @Test
    public void testAssertThrowsWithMessage(){
        assertThrows(IOException.class, () -> divide(1, 0), "除以0啦!!!");
    }

    @Test
    public void testAssertThrowsWithMessageSupplier(){
        assertThrows(Exception.class, () -> divide(1, 0), () -> "除以0啦!!!");
    }

    private int divide(int a, int b){
        return a / b;
    }
}

在以上测试代码中,三个测试用例都使用​​assertThrows()​​,分别期望不同的异常类型,对应的可执行代码为调用divide方法,用1除以0。


执行该测试代码结果如下:

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

  • testAssertThrows() : 该用例期望抛出ArithmeticException,因为1除以0抛出的异常就是ArithmeticException,所以该用例通过;
  • testAssertThrowsWithMessage():该用例期望抛出IOException,ArithmeticException并不是IOException的子类,所以测试未通过;
  • testAssertThrowsWithMessageSupplier():该用例期望抛出Exception,ArithmeticException是Exception的自雷,所以测试通过。


使用​​Supplier<String> messageSupplier​​​参数的断言相比使用​​String message​​参数断言有一个优点,就是只有在断言不通过的时候,才会构造字符串对象。

assertTimeout()

该断言方法它用于测试长时间运行的任务。


如果测试用例中的给定任务花费的时间超过指定时间,则测试用例将失败。


提供给测试用例的任务将与调用代码在同一个线程中执行。此外,如果超时,并不会抢先中止任务的执行。


该方法同样有三个重载实现:

public static void assertTimeout(Duration timeout, Executable executable)
  
public static void assertTimeout(Duration timeout, Executable executable, String message)
  
public static void assertTimeout(Duration timeout, Executable executable, Supplier<String> messageSupplier)

通过以下测试代码来看一下该断言方法如何执行:

package test;

import org.junit.jupiter.api.Test;

import java.time.Duration;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTimeout;

/**
 * @author 小黑说Java
 * @ClassName AssertTimeoutTest
 * @Description
 * @date 2022/1/6
 **/
public class AssertTimeoutTest {
    @Test
    void timeoutNotExceeded(){
        // 该断言成功
        assertTimeout(Duration.ofMinutes(3), () -> {
            // 执行不到3分钟的任务
        });
    }

    @Test
    void timeoutNotExceededWithResult(){
        // 该断言执行成功并返回对象
        String actualResult = assertTimeout(Duration.ofMinutes(3), () -> {
            return "result";
        });
        assertEquals("result", actualResult);
    }

    @Test
    void timeoutNotExceededWithMethod(){
        // 该断言调用一个方法引用并返回一个对象
        String actualGreeting = assertTimeout(Duration.ofMinutes(3), AssertTimeoutTest::greeting);
        assertEquals("Hello, World!", actualGreeting);
    }

    @Test
    void timeoutExceeded(){
        // 以下断言失败,并显示类似于以下内容的错误消息:
        // execution exceeded timeout of 10 ms by 91 ms
        assertTimeout(Duration.ofMillis(10), () -> {
            // 模拟耗时超过10毫秒的任务
            Thread.sleep(100);
            System.out.println("结束");
        });
    }

    private static String greeting(){
        return "Hello, World!";
    }

}

执行以上测试用例结果如下:

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

除了timeoutExceeded()其他测试用例都通过。并且从日志中可以看到,有打印出“结束”,说明并未将任务执行线程中断。

assertTimeoutPreemptively()

该断言方法和assertTimeout()作用基本相同,但是有一个主要的区别,使用该断言提供给测试用例的任务将在与调用代码不同的线程中执行。并且,如果执行超时,任务的执行将被抢先中止。

package test;

import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import java.time.Duration;
import org.junit.jupiter.api.Test;

/**
 * @author 小黑说Java
 * @ClassName AssertTimeoutPreemptivelyTest
 * @Description
 * @date 2022/1/6
 **/
public class AssertTimeoutPreemptivelyTest {
    @Test
    void timeoutExceededWithPreemptiveTermination(){
        // 以下断言失败,并显示类似于以下内容的错误消息:
        // execution timed out after 10 ms
        assertTimeoutPreemptively(Duration.ofMillis(10), () -> {
            // 模拟耗时超过10毫秒的任务
            Thread.sleep(100);
            System.out.println("结束");
        });
    }
}

执行该测试案例结果如下,可以看到,并没有打印出“结束”。

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

断言是​​org.juit.jupiter.Assertions​​类中的一系列静态方法,其作用就是在测试中用来验证预期行为。

什么是“假设”?

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

在JUnit5中的​​org.junit.jupiter.api.Assumptions​​类中定义了一系列的支持基于假设的条件执行的测试方法。


与断言相比,如果假设方法失败并不会导致测试用例的失败,只会让测试用例中止。


假设通常在中断没有意义的测试方法时使用。例如,如果测试依赖于当前运行时环境中并不存在的内容,那么这个测试案例就没有测试意义。


如果假设不满足,则会抛出​​TestAbortedException​​。


Junit 5中有3种类型的假设:


assumeTrue() : 假设为真。


assumeFalse(): 假设为假。


assumeThat(): 假设是某种特定情况。

assumeTrue()

该方法假设给定的参数是true,如果传入的参数为true,则测试继续执行;如果假设为false,则该测试方法中止。


assumeTrue()有三类类型的重载方法:

// 使用boolean作为假设验证
public static void assumeTrue(boolean assumption) throws TestAbortedException
public static void assumeTrue(boolean assumption, Supplier<String> messageSupplier) throws TestAbortedException
public static void assumeTrue(boolean assumption, String message) throws TestAbortedException

// 使用BooleanSupplier作为假设验证
public static void assumeTrue(BooleanSupplier assumptionSupplier) throws TestAbortedException
public static void assumeTrue(BooleanSupplier assumptionSupplier, String message) throws TestAbortedException
public static void assumeTrue(BooleanSupplier assumptionSupplier, Supplier<String> messageSupplier) throws TestAbortedException

接下来我们看一下上面的方法如何使用:

package test;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assumptions.assumeTrue;

/**
 * @author 小黑说Java
 * @ClassName AssumeTrueTest
 * @Description
 * @date 2022/1/6
 **/
public class AssumeTrueTest {
    @Test
    void testOnDevelopmentEnvironment(){
        System.setProperty("ENV", "DEV");
        assumeTrue("DEV".equals(System.getProperty("ENV")));
        //后续代码会继续执行
        System.out.println("开发环境测试");
    }

    @Test
    void testOnProductionEnvironment(){
        System.setProperty("ENV", "PROD");
        assumeTrue("DEV".equals(System.getProperty("ENV")), "假设失败");
        // 后续代码不会执行
        System.out.println("生产环境测试");
    }
}

运行结果如下,​​testOnProductionEnvironment()​​因为假设结果为false,所以中断,并没有打印输出语句。

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

assumeFalse()

assumeFalse()和assumeTrue()同理,验证给定的假设是否为false,如果为false,则测试继续,如果为true,测试中止。

assumingThat()

假设JUnit5中的API有一个静态实用程序方法,名为AssergingThat()。 该假设方法接收一个Boolean或BooleanSupplier作为假设,对该假设进行验证,如果验证通过,则执行第二个参数传入的Executable;如果假设验证不通过,则不执行;不管是否验证通过,都不会影响测试用例后续的执行。

public static void assumingThat(boolean assumption, Executable executable)
public static void assumingThat(BooleanSupplier assumptionSupplier, Executable executable)

我们通过下面代码来验证一下该假设方法:

package test;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumingThat;

/**
 * @author 小黑说Java
 * @ClassName AssumingThatTest
 * @Description
 * @date 2022/1/6
 **/
public class AssumingThatTest {

    @Test
    void testInAllEnvironments(){
        System.setProperty("ENV", "DEV");
        assumingThat("DEV".equals(System.getProperty("ENV")),
                () -> {
                    System.out.println("testInAllEnvironments - 只在开发环境执行!!!");
                    assertEquals(2, 1 + 1);
                });
        assertEquals(42, 40 + 2);
    }

    @Test
    void testInAllEnvironments2(){
        System.setProperty("ENV", "DEV");
        assumingThat("PROD".equals(System.getProperty("ENV")),
                () -> {
                    System.out.println("testInAllEnvironments2 - 只在生产环境执行 !!!");
                    assertEquals(2, 1 + 1);
                });
        assertEquals(42, 40 + 2);
    }
}

运行结果如下,可以看到只有testInAllEnvironments()中的Executable执行了:

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

生命周期相关注解

在Junit5中,对于每个@Test,都会创建一个测试类的新实例。例如,如果一个类有两个@Test方法,那么将创建两个测试类实例,每个@Test一个。因此,测试类的构造函数被调用的次数与@Test方法的数量一样多。


我们可以通过如下代码来证明这一点:

package test;

import org.junit.jupiter.api.Test;

/**
 * @author 小黑说Java
 * @ClassName LifecycleTest
 * @Description
 * @date 2022/1/6
 **/
public class LifecycleTest {

    public LifecycleTest(){
        System.out.println("LifecycleTest - 创建实例 !!!");
    }

    @Test
    public void testOne(){
        System.out.println("LifecycleTest - testOne()执行!!!");
    }

    @Test
    public void testTwo(){
        System.out.println("LifecycleTest - testTwo()执行!!!");
    }

}

从以下执行结果我们可以看出,确实执行了两次构造方法。这表明每个测试方法都是在各自的测试对象实例中执行。

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

@BeforeEach和@AfterEach

顾名思义,使用@BeforeEach和@AfterEach注释的方法在每个@Test方法之前和之后调用。因此,如果测试类中有两个@Test方法,则@BeforeEach方法将在测试方法之前被调用两次,@AfterEach方法同理。

package test;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
 * @author 小黑说Java
 * @ClassName LifecycleTest
 * @Description
 * @date 2022/1/6
 **/
public class LifecycleTest {

    public LifecycleTest(){
        System.out.println("LifecycleTest - 创建实例 !!!");
    }

    @BeforeEach
    public void beforeEach(){
        System.out.println("LifecycleTest - beforeEach()执行!!!");
    }

    @Test
    public void testOne(){
        System.out.println("LifecycleTest - testOne()执行!!!");
    }

    @Test
    public void testTwo(){
        System.out.println("LifecycleTest - testTwo()执行!!!");
    }

    @AfterEach
    public void afterEach(){
        System.out.println("LifecycleTest - afterEach()执行 !!!");
    }

}

执行结果如下:

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

因为Junit测试类有两个@Test方法,所以它在测试类的单独实例中执行每个测试方法。随机选择一个@Test方法,按照如下顺序执行:


  1. 执行构造方法创建实例;
  2. 执行@BeforeEach注解方法;
  3. 执行@Test方法;
  4. 执行@AfterEach注解方法。


然后,再按照相同的步骤执行下一个@Test方法。


通常,当我们拥有跨各种测试用例的公共设置逻辑时,公共初始化代码放在@BeforeEach方法中,并在@AfterEach方法中进行清理。

@BeforeAll和@AfterAll

顾名思义,@BeforeAll是在所有测试用例执行之前执行,@AfterAll是在所有测试用例执行之后执行。


这两个注解可@BeforeEach和@AfterEach还有一点不同,就是只能修改在静态方法上。这里也很好理解,因为每个@Test方法都是单独的测试对象,要在所有用例前执行的方法必然不能是某一个对象的方法。

package test;

import org.junit.jupiter.api.*;

/**
 * @author 小黑说Java
 * @ClassName LifecycleTest
 * @Description
 * @date 2022/1/6
 **/
public class LifecycleTest {

    @BeforeAll
    public static void beforeAll(){
        System.out.println("LifecycleTest - beforeAll()执行!!!");
    }

    public LifecycleTest(){
        System.out.println("LifecycleTest - 创建实例 !!!");
    }

    @BeforeEach
    public void beforeEach(){
        System.out.println("LifecycleTest - beforeEach()执行!!!");
    }

    @Test
    public void testOne(){
        System.out.println("LifecycleTest - testOne()执行!!!");
    }

    @Test
    public void testTwo(){
        System.out.println("LifecycleTest - testTwo()执行!!!");
    }

    @AfterEach
    public void afterEach(){
        System.out.println("LifecycleTest - afterEach()执行 !!!");
    }

    @AfterAll
    public static void afterAll(){
        System.out.println("LifecycleTest - afterAll()执行!!!");
    }

}

执行以上代码结果如下:

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

@BeforeAll和@Afterall具有以下特点:


  • 这两种方法都是静态方法。
  • 这两种方法在测试生命周期中都只会调用一次;
  • @BeforeAll方法是类级方法,会在构造函数之前被调用;
  • @AfterAll方法也是类级别的方法,它在所有方法执行后被调用。
  • @BeforeAll方法常用于需要进行资源初始化的地方,比如数据库连接、服务器启动等。这些资源可以被测试方法使用。
  • @AfterAll方法常用于需要进行昂贵的资源清理的地方,比如数据库连接关闭、服务器停止等。

自定义名称@DisplayName

JUnit5中的@DisplayName注解用于为测试类自定义名称。默认情况下,JUnit5测试报告会在IDE测试报告中和在执行测试用例时打印测试类的类名。我们可以使用

@DisplayName注解为测试类提供自定义名称,这使其更易于阅读。


@DisplayName不仅可以放在类上,我们还可以加在测试方法上,给每个测试用例自定义名称。


@DisplayName注解可以接受具有以下内容的字符串:


  • 一串单词;
  • 特殊字符;
  • 甚至可以使用表情符号。

@DisplayName("$测试 DisplayName ♥ %^&")
public class DisplayNameTest {

    @Test
    @DisplayName("$用例1☺%^&")
    public void test(){
        System.out.println("test method执行!!!");
    }
}

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

参数解析器ParameterResolver

在Junit5之前的版本中,对在测试类的构造方法或测试方法中使用参数的支持比较有限。


JUnit5的Jupiter中的一个主要变化是现在允许测试构造函数和测试方法都有参数。这些参数为构造函数和测试方法提供元数据。


因此,可以更灵活的支持测试方法和构造函数的依赖项注入。


在JUnit 5中,​​org.juit.jupiter.api.tension​​​包中有一个名为​​ParameterResolver​​的接口。该接口定义了希望在运行时动态解析参数的测试扩展的API。


Junit 5中有多种类型的ParameterResolver。通常,如果测试构造函数或@Test、@BeforeEach、@AfterEach、@BeforeAll、@AfterAll、@TestFactory方法接受参数,则必须在运行时由注册的ParameterResolver解析该参数。

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

TestInfoParameterResolver

​TestInfoParameterResolver​​​是一个内置的​​ParameterResolver​​​,如果方法参数的类型为​​TestInfo​​​,则表示​​TestInfoParameterResolver​​​将提供与当前测试对应的TestInfo实例作为该参数的值。然后,可以使用作为参数的TestInfo来检索有关当前测试的信息或元数据,例如测试的显示名称、测试类、测试方法或关联的标记。 如果在测试方法上使用​​@DisplayName​​注释,则检索与测试方法关联的自定义名称,否则检索技术名称,即测试类或测试方法的实际名称。

package test;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;

/**
 * @author 小黑说Java
 * @ClassName TestInfoParameterTest
 * @Description
 * @date 2022/1/6
 **/
@DisplayName("测试Parameter")
public class TestInfoParameterTest {

    @BeforeAll
    public static void beforeAll(TestInfo testInfo){
        System.out.println("beforeAll() 执行- ");
        System.out.println("Display name - " + testInfo.getDisplayName());
        System.out.println("Test Class - " + testInfo.getTestClass());
        System.out.println("Test Method - " + testInfo.getTestMethod());
        System.out.println("*******************************************");
    }

    public TestInfoParameterTest(TestInfo testInfo){
        System.out.println("Constructor 执行 - ");
        System.out.println("Display name - " + testInfo.getDisplayName());
        System.out.println("Test Class - " + testInfo.getTestClass());
        System.out.println("Test Method - " + testInfo.getTestMethod());
        System.out.println("*******************************************");
    }

    @BeforeEach
    public void beforeEach(TestInfo testInfo){
        System.out.println("beforeEach()执行 - ");
        System.out.println("Display name - " + testInfo.getDisplayName());
        System.out.println("Test Class - " + testInfo.getTestClass());
        System.out.println("Test Method - " + testInfo.getTestMethod());
        System.out.println("*******************************************");
    }

    @Test
    @DisplayName("测试用例1")
    public void testOne(TestInfo testInfo){
        System.out.println("testOne() got executed with test info as - ");
        System.out.println("Display name - " + testInfo.getDisplayName());
        System.out.println("Test Class - " + testInfo.getTestClass());
        System.out.println("Test Method - " + testInfo.getTestMethod());
        System.out.println("*******************************************");
    }

}

以上代码执行结果如下:

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

从结果可以看出,传入TestInfo参数,可以在方法中获取到对应的测试用例方法名,以及对应的DisplayName等信息。

禁用测试用例

我们可能需要在某些情况下禁用我们的测试用例,在Junit5中提供了一禁用或启用测试方法或测试类的能力。

@Disabled

该注解可以作用在测试类或测试方法上,如果在测试类上则表示该类下的所有测试方法都关闭;如果作用在方法上,表示该测试方法被关闭。


同时,该注解有一个可选参数,可以作为关闭测试用例的原因。

package test;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * @author 小黑说Java
 * @ClassName DisableTest
 * @Description
 * @date 2022/1/6
 **/
@Disabled
public class DisableTest {
    @Test
    void evenNumberTrue(){
        OddEven oddEven = new OddEven();
        assertTrue(oddEven.isNumberEven(10));
    }

    @Test
    @Disabled("因为XXX关闭该用例")
    void oddNumberFale(){
        OddEven oddEven = new OddEven();
        assertFalse(oddEven.isNumberEven(11));
    }

}

Junit5还提供了其他各种可以对测试用例进行禁用和启用的注解,这里不在重复讲解。如果你有兴趣可以通过Junit 5中jupiter官方API了解更多.

嵌套测试

@Nested注解可以支持对多个测试进行分组,来建立各个测试用例之间的关系。通常是在主测试类中添加内部类的方式来实现。


但是,默认情况下,内部类并不参与测试执行,只有使用@Nested注解修饰的内部类才能具备测试功能。

package test;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import java.util.LinkedList;
import java.util.Queue;

import static org.junit.jupiter.api.Assertions.*;

/**
 * @author 小黑说Java
 * @ClassName TestingAQueueTest
 * @Description
 * @date 2022/1/6
 **/
public class TestingAQueueTest {
    // 字符串队列
    Queue<String> queue;

    @Test
    @DisplayName("is null")
    void isNotInstantiated(){
        assertNull(queue);
    }

    @Nested
    @DisplayName("when new")
    class WhenNew {

        @BeforeEach
        void createNewStack(){
            queue = new LinkedList<>();
        }

        @Test
        @DisplayName("is empty")
        void isEmpty(){
            assertTrue(queue.isEmpty());
        }

        @Test
        @DisplayName("return null element when polled")
        void returnNullWhenPolled(){
            assertNull(queue.poll());
        }

        @Test
        @DisplayName("return null element when peeked")
        void returnNullWhenPeeked(){
            assertNull(queue.peek());
        }

        @Nested
        @DisplayName("after offering an element")
        class AfterOffering {

            String anElement = "an element";

            @BeforeEach
            void offerAnElement(){
                queue.offer(anElement);
            }

            @Test
            @DisplayName("it is no longer empty")
            void isNotEmpty(){
                assertFalse(queue.isEmpty());
            }

            @Test
            @DisplayName("returns the element when polled and is empty")
            void returnElementWhenPolled(){
                assertEquals(anElement, queue.poll());
                assertTrue(queue.isEmpty());
            }

            @Test
            @DisplayName("returns the element when peeked but remains not empty")
            void returnElementWhenPeeked(){
                assertEquals(anElement, queue.peek());
                assertFalse(queue.isEmpty());
            }
        }
    }
}

执行结果如下:

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

通过@Nested注解,可以让我们的测试用例更有结构,从逻辑上我们可以将测试用例进行分组,更有可读性。

重复测试

Junit5的jupiter中提供了重复测试指定次数的能力。 使用​​@repeatedtest​​注解的方法来完成。 也可通过@DisplayName自定义重复测试的名称。

package test;


import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;

/**
 * @author 小黑说Java
 * @ClassName RepeatedTest
 * @Description
 * @date 2022/1/6
 **/
public class RepeateTest {

    @RepeatedTest(5)
    public void RepeatedTest(){
        assertTrue(0 < 5);
    }

    @RepeatedTest(name = "{displayName} - {currentRepetition}/{totalRepetitions}", value = 5)
    @DisplayName("Repeated test")
    public void repeatedTestWithDisplayName(){
        assertTrue(0 < 5);
    }
}

运行结果如下:

没写单测出BUG,该学一下Junit5了-鸿蒙开发者社区

总结

Junit是Java中非常流行的测试框架,在我们日常开发中编写单元测试会经常用到,新版本的Junit5包含Platform、Jupiter、Vintage三个模块。


在Junit5中新出现了很多创新,支持Java8+的新功能,以及一些不同的测试风格,让我们在单元测试时能更灵活,拥抱变化,持续学习。


如果觉得本文对你有用,可以点击收藏,如果能给小黑点个,那就再好不过啦。


我是小黑,一名在互联网"苟且"的程序员

流水不争先,贵在滔滔不绝

标签
已于2022-11-9 15:50:05修改
收藏
回复
举报
回复
    相关推荐