如何使用 JUnit5 框架?

JUnit5 单元测试框架使用教程

一、Junit5 是什么?

Junit5是一个用于在Java平台上进行单元测试的框架。JUnit 5 框架主要由三部分组成:JUnit Platform、JUnit Jupiter 和 JUnit Vintage。

  • JUnit Platform:定义了测试引擎的 API,是 JVM 上用于启动测试框架的基础服务,支持通过 IDE、构建工具、命令行等方式运行单元测试。
  • JUnit Jupiter:包含 JUnit 5 新的编程模型和扩展模型,主要用于编写和扩展测试代码。
  • JUnit Vintage:兼容运行 JUnit 3 和 JUnit4 编写的测试用例。

二、Junit5 的注解

(一)导入依赖

导入五个依赖:

java 复制代码
        <!--  junit-jupiter-api 里有 @BeforeAll......等注解  -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.10.0</version>
        </dependency>
            
        <!--   junit-jupiter-params 里有 @ValueSource......等注解    -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-params</artifactId>
            <version>5.10.0</version>
        </dependency>
            
        <!--    用于运行 识别上述注解    -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.10.0</version>
            <scope>test</scope>
        </dependency>

        <!-- 测试套件  -->
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-suite-api</artifactId>
            <version>1.10.0</version>
        </dependency>
            
        <!-- 运行测试套件的测试引擎-->
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-suite-engine</artifactId>
            <version>1.10.0</version>
            <scope>test</scope>
        </dependency>

(二)常用的注解

如果你的IDEA在使用JUnit注解的时候发生如下情况:依赖已经导入且加载完成,但是IDEA没能识别出来,如图:

有的注解也在params包中。

(我真的不理解为啥)

有一种解决办法:

点击后,选择相应的版本,我这里是5.10.0,点了之后IDEA就能识别出来了。

1.@Test

@Test 表示方法是测试方法,有多个 @Test 方法就执行多少个。

java 复制代码
public class JUnitTest {

    @Test
    void test0(){
        System.out.println("测试用例1");
    }

    @Test
    void test1(){
        System.out.println("测试用例2");
    }

    @Test
    void test2(){
        System.out.println("测试用例3");
    }
}

结果:

2.@BeforeAll、@AfterAll

  • @BeforeAll:表示被注解的方法应该在当前类的所有 @Test,@RepeatedTest,@ParameterizedTest和@TestFactory方法之前执行;
  • @AfterAll:表示被注解的方法应该在当前类的所有 @Test,@RepeatedTest,@ParameterizedTest和@TestFactory方法之后执行;

使用@BeforeAll@AfterAll注解的方法要加上static

java 复制代码
public class JUnitTest {
    @BeforeAll
    static void beforeAll(){
        //可以用于创建一些资源
        System.out.println("我是BeforeAll,我最开始执行。");
    }
    @AfterAll
    static void afterAll(){
        //可以用于释放资源
        System.out.println("我是AfterAll,我最后执行。");
    }
    @Test
    void test0(){
        System.out.println("测试用例1");
    }
    @Test
    void test1(){
        System.out.println("测试用例2");
    }
    @Test
    void test2(){
        System.out.println("测试用例3");
    }
}

3.@BeforeEach、@AfterEach

  • @BeforeEach:表示被注解的方法应在当前类的每个 @Test,@RepeatedTest,@ParameterizedTest或@TestFactory方法之前执行;
  • @AfterEach:表示被注解的方法应在当前类的每个 @Test,@RepeatedTest,@ParameterizedTest或@TestFactory方法之后执行;
java 复制代码
public class JUnitTest {
    @BeforeAll
    static void beforeAll(){
        System.out.println("我是BeforeAll,我最开始执行。");
    }
    @AfterAll
    static void afterAll(){
        System.out.println("我是AfterAll,我最后执行。");
    }
    @BeforeEach
    void beforeEach(){
        System.out.println("我是BeforeEach,我在每个 @Test 前执行。");
    }
    @AfterEach
    void afterEach(){
        System.out.println("我是AfterEach,我在每个 @Test 后执行。");
    }
    @Test
    void test0(){
        System.out.println("测试用例1");
    }
    @Test
    void test1(){
        System.out.println("测试用例2");
    }
    @Test
    void test2(){
        System.out.println("测试用例3");
    }
}

结果:

4.@Disabled

@Disabled用于禁用测试类或测试方法,添加该注解的方法不会被测试。

java 复制代码
public class JUnitTest {
    @BeforeAll
    static void beforeAll(){
        System.out.println("我是BeforeAll,我最开始执行。");
    }
    @AfterAll
    static void afterAll(){
        System.out.println("我是AfterAll,我最后执行。");
    }
    @BeforeEach
    void beforeEach(){
        System.out.println("我是BeforeEach,我在每个 @Test 前执行。");
    }
    @AfterEach
    void afterEach(){
        System.out.println("我是AfterEach,我在每个 @Test 后执行。");
    }
    @Test
    @Disabled //忽略测试用例1
    void test0(){
        System.out.println("测试用例1");
    }
    @Test
    void test1(){
        System.out.println("测试用例2");
    }
    @Test
    void test2(){
        System.out.println("测试用例3");
    }
}

结果:

(三)参数化测试

1.@ParameterizedTest + @ValueSource

@ParameterizedTest的作用就是可以用不同的参数多次运行测试。但是必须声调用提供参数的来源(source)。

@ValueSource它可以让你指定一个原生类型(String,int,long或double)的数组,并且只能为每次调用提供一个参数。

java 复制代码
public class JUnitTest {

    @BeforeAll
    static void beforeAll(){
        System.out.println("我是BeforeAll,我最开始执行。");
    }

    @AfterAll
    static void afterAll(){
        System.out.println("我是AfterAll,我最后执行。");
    }

    @BeforeEach
    void beforeEach(){
        System.out.println("我是BeforeEach,我在每个 Test 前执行。");
    }

    @AfterEach
    void afterEach(){
        System.out.println("我是AfterEach,我在每个 Test 后执行。");
    }

    @Test
    void test0(){
        System.out.println("测试用例1");
    }

    @Test
    void test1(){
        System.out.println("测试用例2");
    }

    @Test
    void test2(){
        System.out.println("测试用例3");
    }
    @ParameterizedTest
    @ValueSource(strings = {"小明","小红","小兰"})
    void paramTest(String name){
        System.out.println(name);
    }
}

结果:

2.@ParameterizedTest + @CsvSource

@CsvSource允许将参数列表表示为以逗号分隔的值(例如,字符串文字)。

java 复制代码
public class JUnitTest { 
    @ParameterizedTest
    @CsvSource({"小明, 1","小红,2","小兰,3"})
    void csvSource(String name,int id){
        System.out.println(name + ":" + id);
    }
}

结果:

@CsvSource使用'作为转义字符。

示例输入 结果字符列表
@CsvSource({ "foo, bar" }) "foo", "bar"
@CsvSource({ "foo, 'baz, qux'" }) "foo", "baz, qux"
@CsvSource({ "foo, ''" }) "foo", ""
@CsvSource({ "foo, " }) "foo", null

3.@ParameterizedTest + @CsvFileSource

@CsvFileSource让你使用classpath中的CSV文件。CSV文件中的每一行都会导致参数化测试的一次调用。

resources目录下创建csv文件:

test.csv:

csv 复制代码
小明, 1
小红, 2
"小明, 小红", 3
java 复制代码
public class JUnitTest {
    @ParameterizedTest
    @CsvFileSource(resources = "/test.csv")
    void csvFile(String name,int id){
        System.out.println(name + ": " + id);
    }
}

结果:

@CsvSource中使用的语法相反,@CsvFileSource使用双引号"作为转义字符。通过上面的代码就可以看出来。一个空的转义值""会产生一个空字符串, 一个完全为空的值被解释为null引用。

4.@ParameterizedTest + @MethodSource

@MethodSource允许引用一个或多个测试类的工厂方法。

java 复制代码
public class JUnitTest {
    @ParameterizedTest
    @MethodSource("stringProvider") //指定方法
    void methodSource(int age,String name){
        System.out.println(age + ": " + name);
    }
    static Stream<Arguments> stringProvider() {
        return Stream.of(
                Arguments.arguments(12,"李四"),
                Arguments.arguments(18,"王五"),
                Arguments.arguments(20,"小红")
        );
    }
}

@MethodSource注解表示这个方法的参数来源于一个名为stringProvider的静态方法。stringProvider方法返回一个Stream<Arguments>类型的对象,其中每个Arguments对象包含了一组用于测试的参数。

(四)测试方法的执行顺序

1.@TestMethodOrder + @Order

在 JUnit5 中,测试方法执行的顺序是不确定的或者是根据方法首字母来排序的。

java 复制代码
public class JUnitTest2 {
    @Test
    void C(){
        System.out.println("A");
    }
    @Test
    void B(){
        System.out.println("B");
    }
    @Test
    void A(){
        System.out.println("C");
    }
}

结果:

让执行顺序为 C、B、A:

java 复制代码
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class JUnitTest2 {
    int a = 0;
    @Test
    @Order(1)
    void C(){
        a++;
        System.out.println(a);
        System.out.println("C");
    }
    @Test
    @Order(2)
    void B(){
        a++;
        System.out.println(a);
        System.out.println("B");
    }
    @Test
    @Order(3)
    void A(){
        a++;
        System.out.println(a);
        System.out.println("A");
    }
}

首先在类上添加@TestMethodOrder(MethodOrderer.OrderAnnotation.class),然后再为每个方法上添加@Order()注解,值越小越优先被执行。

(五)测试实例的生命周期

1.@TestInstance

我添加一个成员变量,每次执行测试方法的时候都++一次。

java 复制代码
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
public class JUnitTest2 {
    int a = 0;

    @Test
    @Order(1)
    void A(){
        a++;
        System.out.println("A方法:" + a);
        System.out.println("A");
    }

    @Test
    @Order(2)
    void B(){
        a++;
        System.out.println("B方法:" + a);
        System.out.println("B");
    }

    @Test
    @Order(3)
    void C(){
        a++;
        System.out.println("C方法:" + a);
        System.out.println("C");
    }
}

结果:

为了允许隔离执行单个的测试方法,JUnit在执行每个测试方法之前会创建每个测试类的新实例。如果想改变策略,就要用@TestInstance,在类上添加@TestInstance(TestInstance.Lifecycle.PER_CLASS)

java 复制代码
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class JUnitTest2 {
    int a = 0;

    @Test
    @Order(1)
    void A(){
        a++;
        System.out.println("A方法:" + a);
        System.out.println("A");
    }

    @Test
    @Order(2)
    void B(){
        a++;
        System.out.println("B方法:" + a);
        System.out.println("B");
    }

    @Test
    @Order(3)
    void C(){
        a++;
        System.out.println("C方法:" + a);
        System.out.println("C");
    }
}

结果:

Lifecycle.PER_CLASS表示只创建一个实例。不添加注解的时候,默认是Lifecycle.PER_METHOD

当使用这种模式时,每个测试类将创建一个新的测试实例。因此,如果测试方法依赖于存储在实例变量中的状态,则可能需要在@BeforeEach@AfterEach方法中重置该状态(重置变量的值)。

(六)断言 Assertions

断言方法 描述
assertEquals(expected, actual) 检查两个值是否相等,如果不相等则抛出AssertionError
assertNotEquals(expected, actual) 检查两个值是否不相等,如果相等则抛出AssertionError
assertTrue(condition) 检查一个条件是否为真,如果为假则抛出AssertionError
assertFalse(condition) 检查一个条件是否为假,如果为真则抛出AssertionError
assertNull(object) 检查一个对象是否为null,如果不为null则抛出AssertionError
assertNotNull(object) 检查一个对象是否不为null,如果为null则抛出AssertionError
assertSame(expected, actual) 检查两个对象是否是同一个实例,如果不是则抛出AssertionError
assertNotSame(expected, actual) 检查两个对象是否不是同一个实例,如果是则抛出AssertionError
assertArrayEquals(expected, actual) 检查两个数组是否相等,如果不相等则抛出AssertionError
assertTimeout(duration, executable) 检查一个可执行的代码块是否在指定的时间内完成,如果超时则抛出AssertionError
java 复制代码
public class JUnitTest3 {
    @Test
    void assertEqualsDemo(){
        int num = 10;
        Assertions.assertEquals(1,num,"不符合预期");
    }
    @Test
    void assertTrueDemo(){
        int num = 10;
        Assertions.assertTrue(num > 10,"不符合预期");
    }
    @Test
    void assertTimeoutDemo(){
        int num = 10;
        Assertions.assertTimeout(Duration.ofSeconds(3), new Executable() {
            @Override
            public void execute() throws Throwable {
                //代码块
                Thread.sleep(4000);
            }
        });
    }
}

结果:

(七)测试套件

测试套件是一组相关的测试,可以一起运行,以便更方便地组织和管理测试。使用套件要引入两个依赖:junit-platform-suite-apijunit-platform-suite-engine,具体的在文章开头。

套件其实很好理解,就是使几个类同时进行测试。

1.@SelectClasses

java 复制代码
@Suite
@SelectClasses(value = {JUnitTest.class,JUnitTest2.class})
public class RunSuite {

}

@Suite的作用是将一个类标记为JUnit平台上的测试套件。

@SelectClasses指定在JUnit平台上运行测试套件时要选择的类。

运行结果:

2.@SelectPackages

可以选择类,那么也可以包。

java 复制代码
@Suite
@SelectPackages(value = {"package1"})
//可以选择多个包:@SelectPackages(value = {"package1","package2","package3"......})
public class RunSuite {

}

结果:

为什么只执行了JUnitTest这一个类?我的JUnitTest2呢?我们来看看它:

IDEA提示我们它的命名不符合规则,那这个规则是什么意思呢?

3.测试类命名规则

  • A-Z\[A-Za-z\\d\]\*Test(s\|Case)?:表示以大写字母开头,后面跟任意个字母或数字,**最后以Test, Tests, TestCase结尾的字符串**,例如MyTest, MyTests, MyTestCase等。

  • IT(.*):表示以IT开头,后面跟任意个任意字符的字符串,例如ITMyClass, ITMyMethod等。
  • (.*)IT(Case)?:表示以任意个任意字符开头,后面跟IT或者ITCase的字符串,例如MyClassIT, MyMethodITCase等。

其实就是我们的类命名不规范导致框架识别不出来。改类名后:

还有其它很多的注解,这里就不介绍了,篇幅有点长了。官方文档:org.junit.platform.suite.api (JUnit 5.10.0 API)

相关推荐
Apifox5 小时前
提交代码后如何自动触发 Apifox 的自动化测试?
前端·后端·测试
ly1562 天前
pytest-selenium的作用和常用操作-来自ai-个人留存
测试
VyrnSynx2 天前
CodeBuddy IDE震撼来袭:让开发更高效的全新工具,前沿内测大揭秘!
测试·笔记测评
莫魂魂3 天前
07.自动化测试常用函数
测试
胡斌附体3 天前
linux测试端口是否可被外部访问
linux·运维·服务器·python·测试·端口测试·临时服务器
智云软件测评服务4 天前
第三方软件测试检测机构能提供啥测试服务?功能测试了解下
测试·软件·
June bug5 天前
【软考中级·软件评测师】下午题·面向对象测试之架构考点全析:分层、分布式、微内核与事件驱动
经验分享·分布式·职场和发展·架构·学习方法·测试·软考
郝同学的测开笔记15 天前
从 "不支持的URL" 错误谈 IPv6 兼容性与 HTTPDNS 优化
测试
chao_78917 天前
作为测试人员,平时用什么大模型?怎么用?
面试·大模型·测试
chao_78919 天前
死锁相关知识
网络协议·游戏·测试·死锁