天天看點

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的測試上。最後附上我的

測試小例子

,有興趣的同學可以看看。

繼續閱讀