JUnit 5 简介

著名的Java单元测试框架Junit 4已经出来很长时间了,当时我发现JUnit 5已经处于测试版,就准备写文章来介绍JUnit 5.不过因为还是测试版,所以有些地方还不太完善,我也有点懒没有好好写。这几天突然想起这事了,在到官网上查看,发现就在9月10日,JUnit 5的正式版终于出来了!那么我就正好把文章重新好好写写,为大家介绍这个最新的JUnit框架。

框架结构

和JUnit 4相比,JUnit 5的结构非常清晰,为自定义插件、IDE测试执行等扩展功能做了很好的支持。这一点从项目结构就可以看出来。

JUnit Platform

这一组的包名是org.junit.platform,从名字就可以看到,这一组的主要功能就是作为测试框架的基础平台。这个包下的模块包含基础API、执行引擎及执行器、基本的命令行执行功能、命令行界面、Maven及Gradle的测试插件等最基本的功能。

JUnit Jupiter

Jupiter 是JUnit 5的代号,这个包下的模块包含JUnit 5的主要功能。如果我们要使用JUnit 5,那么必然要包含这一组模块。

JUnit Vintage

Vintage 是旧版本JUnit 的代号,这个包下的模块可以让我们在新的JUnit平台上运行旧的JUnit 3 和 4 的测试。

导入类库

在JUnit 5还在测试阶段的时候,官方文档上还有在Maven和Gradle中集成JUnit 5的例子。但是到了正式版,这一部分的内容消失了,仅仅留下两个示例项目的链接,让我们自己参考(复制粘贴)。

使用Maven

junit5-maven-consumer 是官方的Maven例子。本来我准备把相关的POM配置贴到这里,但是一看Maven的配置太长了,所以还是算了。如果有需求的话请自己查看这个项目的POM配置。

使用Gradle

如果用Gradle的话,那么这个问题就简单多了。在junit5-gradle-consumer 示例项目中也有比较详细的说明。

首先,Gradle默认不支持JUnit 5,,所以需要启用JUnit Platform Gradle 插件来支持。

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.0'
    }
}

apply plugin: 'org.junit.platform.gradle.plugin'

然后是关于这个Gradle插件的配置。默认情况下所有的引擎和标签都会被执行。如果你想选择只执行某些引擎和标签的测试,可以取消下面的注释并按照你自己的需求进行修改。当然假如你没有这些高级需求,可以把这一部分删掉。

junitPlatform {
    // platformVersion '1.0.0'
    filters {
        engines {
            // include 'junit-jupiter', 'junit-vintage'
            // exclude 'custom-engine'
        }
        tags {
            // include 'fast'
            exclude 'slow'
        }
        // includeClassNamePattern '.*Test'
    }
    // enableStandardTestTask true
    // reportsDir file('build/test-results/junit-platform') // this is the default
    // logManager 'org.apache.logging.log4j.jul.LogManager'
}

如果你只需要运行JUnit 5测试,只需要导入下面两个依赖项。JUnit Platform的依赖会自动导入。

dependencies {
    testCompile("org.junit.jupiter:junit-jupiter-api:5.0.0")
    testRuntime("org.junit.jupiter:junit-jupiter-engine:5.0.0")
}

如果你想在新平台下运行旧的JUnit 3和4测试,需要导入下面的依赖项。

dependencies {
    testCompile("junit:junit:4.12")
    testRuntime("org.junit.vintage:junit-vintage-engine:4.12.0")
}

编写测试

JUnit 4测试

如果前面都配置好了,现在就可以开始编写测试了。首先先来复习一下旧的JUnit 4测试。

public class JUnit4Test {
    @BeforeClass
    public static void init() {
        System.out.println("Before Class");
    }

    @AfterClass
    public static void clean() {
        System.out.println("After class");
    }

    @Before
    public void before() {
        System.out.println("Before");
    }

    @After
    public void after() {
        System.out.println("After");
    }

    @Test
    public void test1() {
        System.out.println("Test 1");
    }

    @Test
    public void test2() {
        System.out.println("Test 2");
    }
}

使用gradle test等命令执行一下,就会执行这个测试。结果类似于这样。

Before Class
Before
Test 1
Test 2
After
After class

JUnit 5测试

让我们来看看等效的JUnit 5测试怎么写。可以看到最明显的变化:首先几个注解被重新命名成更见名知义的名称;另外一点是测试方法不必是公有方法,这样我们可以少敲几下键盘。

public class JUnit5Test {
    @BeforeAll
    static void beforeAll() {
        System.out.println("Before All");
    }

    @AfterAll
    static void afterAll() {
        System.out.println("After All");
    }

    @BeforeEach
    void before() {
        System.out.println("Before");
    }

    @AfterEach
    void after() {
        System.out.println("After");
    }

    @Test
    void test1() {
        System.out.println("Test 1");
    }

    @Test
    void test2() {
        System.out.println("Test 2");
    }
}

编写断言

为了验证测试用例是否正确,我们需要编写一些断言。JUnit 5自带了很多断言,可以帮助我们编写测试用例。而且这些断言都带有可以接受lambda表达式的重载版本,非常适合Java 8使用。当然我个人认为断言还是AssertJ更方便一点。

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

public class AssertionDemo {
    @Test
    void testAssertion() {
        assertEquals(10, 10);
        assertTrue(true);
        assertEquals(100, 100, "两个数相等");
        assertAll("数字"
                , () -> assertEquals("name", "name")
                , () -> assertEquals(500, 500));
        assertThrows(InvalidParameterException.class
                , () -> {
                    throw new InvalidParameterException();
                }
        );
        int result = assertTimeout(Duration.ofSeconds(5)
                , () -> {
                    int i = 0, j = 0;
                    while (i <= 100) {
                        for (; j <= 100000; j++)
                            j++;
                        i++;
                    }
                    return i;
                });
        assertEquals(100, result);
    }
}

依赖注入

现在测试类的构造方法和测试方法都可以接受参数了。ParameterResolver接口定义了如何在运行时注入参数的方法。内置的几个可以让我们获取测试用例运行时的信息。

首先是TestInfoParameterResolver。如果方法上有TestInfo类型的实例,JUnit 5框架就会自动注入该实例,这个实例的几个方法可以让我们获取测试类和测试方法的名称、显示名称、标签等信息。

public class DependencyInjectionDemo {
    @Test
    @DisplayName("依赖注入")
    @Tag("test")
    void testDisplayName(TestInfo testInfo) {
        assertEquals("依赖注入", testInfo.getDisplayName());
        assertEquals(Collections.singleton("test"), testInfo.getTags());
    }
}

还有RepetitionInfoParameterResolver等内置参数解析器,将在后面介绍。

常用注解

显示名称

我们可以为测试类和测试方法添加自定义的名称,这些名贵会由测试运行器和测试报告所显示。显示名称没有变量名那样的显示,可以是一段包含空格的长字符串,甚至还可以是Emoji表情。

@DisplayName("测试类可以指定显示名称")
public class DisplayNameDemo {
    @Test
    @DisplayName("测试方法也可以指定显示名称")
    void testWithLongDisplayName() {

    }

    @Test
    @DisplayName("显示名称还可以包含表情😻")
    void testWithDisplayNameWithEmoji() {

    }
}

禁用测试

@Disabled注解可以用到测试类或测试方法上,可以禁用对应的测试。

@Disabled
public class DisabledTestDemo {
    @Test
        //@Disabled
    void testDisabled() {

    }
}

重复测试

如果需要让某个测试方法运行多次,使用@RepeatedTest注解。

public class RepeatedTestDemo {
    @RepeatedTest(10)
    void testRepeated10Times() {
        
    }
}

还可以注入一个实例RepetitionInfo,检查当前重复次数和总的重复次数。

public class RepeatedTestDemo {
    @BeforeEach
    void beforeEach(RepetitionInfo info) {
        System.out.printf("%d - %d\n"
                , info.getCurrentRepetition()
                , info.getTotalRepetitions());
    }

    @RepeatedTest(10)
    void testRepeated10Times() {

    }
}

附带标签

在前面介绍配置Gradle的时候就说了,在配置中可以选择过滤某些标签的测试。要在代码中给标签也很简单,直接用@Tag注解即可。

@Tag("taggedTest")
public class TagDemo {
    @Test
    @Tag("taggedTest1")
    void testWithTag1() {

    }

    @Test
    @Tag("taggedTest2")
    void testWithTag2() {

    }
}

嵌套测试

有时候可能需要嵌套测试来表明某些测试之间的包含关系。嵌套测试使用@Nested注解。

@DisplayName("外层测试")
public class NestedDemo {
    @Test
    void testOuter() {
    }

    @Nested
    @DisplayName("内层测试")
    class InnerTestDemo {
        @Test
        void testInner() {
        }
    }
}

需要注意只有费静态内部类才能使用Nested注解。另外,由于Java不允许内部类有静态方法,所以也不能有@BeforeAll@AfterAll注解。如果想要突破这个限制,需要在嵌套内部类上添加@TestInstance(Lifecycle.PER_CLASS)注解,详情参见Test Instance Lifecycle

IDE支持

虽然现在JUnit 5已经出来了。但是各种工具链的支持还没有跟上。目前只有Intellij IDEA和Eclipse 4.7 (Oxygen)添加了对JUnit 5的支持。所以如果在正式场合的话,使用JUnit 4还是更稳妥一点。

常见问题

区分不同版本间的@Test注解

就在我写这篇文章的时候, 我的测试小例子就遇到了一个问题,测试通不过,显示如下的错误信息。

Failures (1):
  JUnit Vintage:yitian.study.test.AssertionDemo:initializationError
    ClassSource [className = 'yitian.study.test.AssertionDemo', filePosition = null]
    => java.lang.Exception: Method testAssertion() should be public

英文好的同学应该可以认出来,这个错误信息说的是测试方法必须是公开的。但是前面明明说了,JUnit 5取消了这个限制,那么为什么还会出现这个错误呢?我仔细一看,发现了错误所在。可能是由于以前JUnit 4用的比较多,所以IDE默认对于@Test这个注解,自动补全的是这个。

import org.junit.Test;

这个包是JUnit 4下的@Test注解。如果我们要使用JUnit 5的话,需要的是以下这个@Test注解。

import org.junit.jupiter.api.Test;

修改之后,再次运行测试,果然没有问题了。当然这里为了学习和使用,我同时引用了JUnit 4的包,所以才会出现这个冲突。如果没有什么特殊需求的话,建议只导入JUnit 5的jar包,防止出现混淆。当然都导入也可以,只不过你就需要小心区分,不要把JUnit 4的注解写到JUnit 5的测试上。最后附上我的测试小例子,有兴趣的同学可以看看。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,042评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,996评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,674评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,340评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,404评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,749评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,902评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,662评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,110评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,451评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,577评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,258评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,848评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,726评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,952评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,271评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,452评论 2 348

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,628评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,773评论 6 342
  • spring官方文档:http://docs.spring.io/spring/docs/current/spri...
    牛马风情阅读 1,650评论 0 3
  • Instrumentation介绍 Instrumentation是个什么东西? Instrumentation测...
    打不死的小强qz阅读 7,765评论 2 39
  • JUnit 5简介 与之前的版本不同,JUnit 5 由三个不同的模块组成。第一个模块是 JUnit 平台,其主要...
    周润发_223a阅读 2,107评论 0 4