五年了,你還在用Junit4嗎熊楼?

junit5

JUnit5在2017年就發(fā)布了霹娄,你還在用junit4嗎?

什么是junit5

與以前的JUnit版本不同,JUnit 5由三個(gè)不同子項(xiàng)目的多個(gè)不同模塊組成犬耻。

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

JUnit Platform為在JVM上啟動(dòng)測(cè)試框架提供基礎(chǔ)踩晶。它還定義了TestEngine API, 用來開發(fā)在平臺(tái)上運(yùn)行的測(cè)試框架。此外枕磁,平臺(tái)提供了一個(gè)控制臺(tái)啟動(dòng)器]合瓢,用于從命令行啟動(dòng)平臺(tái),并為Gradle和Maven提供構(gòu)建插件以[基于JUnit 4的Runner透典,用于在平臺(tái)上運(yùn)行任意TestEngine晴楔。

JUnit Jupiter是在JUnit 5中編寫測(cè)試和擴(kuò)展的新型編程模型和[擴(kuò)展模型][]的組合.Jupiter子項(xiàng)目提供了TestEngine,用于在平臺(tái)上運(yùn)行基于Jupiter的測(cè)試峭咒。

JUnit Vintage提供TestEngine税弃,用于在平臺(tái)上運(yùn)行基于JUnit 3和JUnit 4的測(cè)試。

為什么需要 JUnit 5

自從有了類似 JUnit 之類的測(cè)試框架凑队,Java 單元測(cè)試領(lǐng)域逐漸成熟则果,開發(fā)人員對(duì)單元測(cè)試框架也有了更高的要求:更多的測(cè)試方式,更少的其他庫的依賴漩氨。

因此西壮,大家期待著一個(gè)更強(qiáng)大的測(cè)試框架誕生,JUnit 作為Java測(cè)試領(lǐng)域的領(lǐng)頭羊叫惊,推出了 JUnit 5 這個(gè)版本款青,主要特性:

  • 提供全新的斷言和測(cè)試注解,支持測(cè)試類內(nèi)嵌
  • 更豐富的測(cè)試方式:支持動(dòng)態(tài)測(cè)試霍狰,重復(fù)測(cè)試抡草,參數(shù)化測(cè)試等
  • 實(shí)現(xiàn)了模塊化,讓測(cè)試執(zhí)行和測(cè)試發(fā)現(xiàn)等不同模塊解耦蔗坯,減少依賴
  • 提供對(duì) Java 8 的支持康震,如 Lambda 表達(dá)式,Sream API等宾濒。

基本注解

@Test: 表示方法是測(cè)試方法腿短。但是與JUnit4的@Test不同,他的職責(zé)非常單一不能聲明任何屬性绘梦,拓展的測(cè)試將會(huì)由Jupiter提供額外測(cè)試

@ParameterizedTest: 表示方法是參數(shù)化測(cè)試

@RepeatedTest: 表示方法可重復(fù)執(zhí)行

@DisplayName: 為測(cè)試類或者測(cè)試方法設(shè)置展示名稱

@BeforeEach: 表示在每個(gè)單元測(cè)試之前執(zhí)行

@AfterEach: 表示在每個(gè)單元測(cè)試之后執(zhí)行

@BeforeAll: 表示在所有單元測(cè)試之前執(zhí)行

@AfterAll: 表示在所有單元測(cè)試之后執(zhí)行

@Tag: 表示單元測(cè)試類別橘忱,類似于JUnit4中的@Categories

@Disabled: 表示測(cè)試類或測(cè)試方法不執(zhí)行,類似于JUnit4中的@Ignore

@Timeout: 表示測(cè)試方法運(yùn)行如果超過了指定時(shí)間將會(huì)返回錯(cuò)誤

@ExtendWith: 為測(cè)試類或測(cè)試方法提供擴(kuò)展類引用

常用注解格式:

class StandardTests {

    //與junit4的@beforeClass類似谚咬,每個(gè)測(cè)試類運(yùn)行一次
    @BeforeAll
    static void initAll() {
    }

    //與junit4中@before類似鹦付,每個(gè)測(cè)試用例都運(yùn)行一次
    @BeforeEach
    void init() {
    }

    @Test
    @DisplayName("成功測(cè)試")
    void succeedingTest() {
    }

    @Test
    @DisplayName("失敗測(cè)試")
    void failingTest() {
        fail("a failing test");
    }

    //禁用測(cè)試用例
    @Test
    @Disabled("for demonstration purposes")
    void skippedTest() {
        // not executed
    }

    @Test
    void abortedTest() {
        assumeTrue("abc".contains("Z"));
        fail("test should have been aborted");
    }


    //與@BeforeEach對(duì)應(yīng)尚粘,每個(gè)測(cè)試類執(zhí)行一次择卦,一般用于恢復(fù)環(huán)境
    @AfterEach
    void tearDown() {
    }

    //與@BeforeAll對(duì)應(yīng),每個(gè)測(cè)試類執(zhí)行一次,一般用于恢復(fù)環(huán)境
    @AfterAll
    static void tearDownAll() {
    }
}

新特性

顯示名稱

@DisplayName("顯示名稱測(cè)試")
class DisplayNameDemo {

    @Test
    @DisplayName("我的 第一個(gè) 測(cè)試 用例")
    void testWithDisplayNameContainingSpaces() {
    }

    @Test
    @DisplayName("╯°□°)╯")
    void testWithDisplayNameContainingSpecialCharacters() {
    }

    @Test
    @DisplayName("??")
    void testWithDisplayNameContainingEmoji() {
    }
}

IDE運(yùn)行測(cè)試結(jié)果顯示:

image-20210416232329161

優(yōu)點(diǎn):通過這種方式秉继,可以在方法名是英文特別長或者很難用英文描述清楚的場(chǎng)景下祈噪,增加中文解釋

更強(qiáng)大的斷言

JUnit Jupiter提供了許多JUnit4已有的斷言方法,并增加了一些適合與Java 8 lambda一起使用的斷言方法尚辑。所有JUnit Jupiter斷言都是[org.junit.jupiter.Assertions]類中的靜態(tài)方法辑鲤。

分組斷言:

多個(gè)條件同時(shí)滿足時(shí)才斷言成功

@Test
void groupedAssertions() {
    Person person = new Person();

    Assertions.assertAll("person",
                         () -> assertEquals("niu", person.getName()),
                         () -> assertEquals(18, person.getAge())
                        );
}

異常斷言:

Junit4時(shí)需要使用rule方式,junit5提供了assertThrows更優(yōu)雅的異常斷言

@Test
void exceptionTesting() {
    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
        throw new IllegalArgumentException("a message");
    });
    assertEquals("a message", exception.getMessage());
}

超時(shí)斷言:

@Test
@DisplayName("超時(shí)測(cè)試")
public void timeoutTest() {
    Assertions.assertTimeout(Duration.ofMillis(100), () -> Thread.sleep(50));
}

標(biāo)簽和過濾

通過標(biāo)簽把測(cè)試分組杠茬,在不同階段執(zhí)行不同的邏輯測(cè)試月褥,比如劃分為快速冒煙測(cè)試和執(zhí)行慢但也重要的測(cè)試

@Test
@Tag("fast")
    void testing_faster() {
}

@Test
@Tag("slow")
    void testing_slow() {
}

然后通過配置maven-surefire-plugin插件

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.22.0</version>
    <configuration>
        <properties>
            <includeTags>fast</includeTags>
            <excludeTages>slow</excludeTages>
        </properties>
    </configuration>
</plugin>

嵌套測(cè)試

當(dāng)我們編寫的類和代碼逐漸增多,隨之而來的需要測(cè)試的對(duì)應(yīng)測(cè)試類也會(huì)越來越多瓢喉。

為了解決測(cè)試類數(shù)量爆炸的問題宁赤,JUnit 5提供了@Nested 注解,能夠以靜態(tài)內(nèi)部成員類的形式對(duì)測(cè)試用例類進(jìn)行邏輯分組栓票。

并且每個(gè)靜態(tài)內(nèi)部類都可以有自己的生命周期方法决左, 這些方法將按從外到內(nèi)層次順序執(zhí)行。

此外走贪,嵌套的類也可以用@DisplayName 標(biāo)記佛猛,這樣我們就可以使用正確的測(cè)試名稱。下面看下簡單的用法:

@DisplayName("A stack")
class TestingAStackDemo {

    Stack<Object> stack;

    @Test
    @DisplayName("is instantiated with new Stack()")
    void isInstantiatedWithNew() {
        new Stack<>();
    }

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

        @BeforeEach
        void createNewStack() {
            stack = new Stack<>();
        }

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


        @Nested
        @DisplayName("after pushing an element")
        class AfterPushing {

            String anElement = "an element";

            @BeforeEach
            void pushAnElement() {
                stack.push(anElement);
            }

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

junit沒有限制嵌套層數(shù)坠狡,除非必要一般不建議使用超過3層继找,過于復(fù)雜的層次結(jié)構(gòu)會(huì)增加開發(fā)者理解用例關(guān)系的難度

構(gòu)造函數(shù)和方法的依賴注入

在之前的所有JUnit版本中,測(cè)試構(gòu)造函數(shù)或方法都不允許有參數(shù)(至少不能使用標(biāo)準(zhǔn)的Runner實(shí)現(xiàn))逃沿。作為JUnit Jupiter的主要變化之一码荔,測(cè)試構(gòu)造函數(shù)和方法現(xiàn)在都允許有參數(shù)。這帶來了更大的靈活性感挥,并為構(gòu)造函數(shù)和方法啟用依賴注入

  • TestInfo可獲取測(cè)試信息
  • TestReporter可以向控制臺(tái)輸出信息
@Test
@DisplayName("test-first")
@Tag("my-tag")
void test1(TestInfo testInfo) {
    assertEquals("test-first", testInfo.getDisplayName());
    assertTrue(testInfo.getTags().contains("my-tag"));
}

@Test
@DisplayName("test-second")
@Tag("my-tag")
void test2(TestReporter testReporter) {
    testReporter.publishEntry("a key", "a value");
}

重復(fù)測(cè)試

多次調(diào)用同一個(gè)測(cè)試用例

@RepeatedTest(10)
@DisplayName("重復(fù)測(cè)試")
public void testRepeated() {
    //...
}
image-20210416232512919

動(dòng)態(tài)測(cè)試

動(dòng)態(tài)測(cè)試只需要編寫一處代碼缩搅,就能一次性對(duì)各種類型的輸入和輸出結(jié)果進(jìn)行驗(yàn)證

@TestFactory
@DisplayName("動(dòng)態(tài)測(cè)試")
Stream<DynamicTest> dynamicTests() {
    List<Person> persons = getAllPerson();

    return persons.stream()
        .map(person -> DynamicTest.dynamicTest(person.getName() + "-test", () -> assertTrue(person.getName().contains("niu"))));
}

超時(shí)測(cè)試

通過時(shí)間來驗(yàn)證用例是否超時(shí),一般要求單個(gè)單元測(cè)試不應(yīng)該超過1秒

class TimeoutDemo {
    @BeforeEach
    @Timeout(5)
    void setUp() {
        // fails if execution time exceeds 5 seconds
    }

    @Test
    @Timeout(value = 1000, unit = TimeUnit.MILLISECONDS)
    void failsIfExecutionTimeExceeds1000Milliseconds() {
        // fails if execution time exceeds 1000 milliseconds
        //也可用這種方式 Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(1500));
    }
}

參數(shù)測(cè)試

參數(shù)測(cè)試我覺得是最好用的特性触幼,可以大量減少重復(fù)模板式代碼硼瓣,也是junit5最驚艷的提升,強(qiáng)烈推薦使用

@ValueSource: 為參數(shù)化測(cè)試指定入?yún)碓粗们С职舜蠡A(chǔ)類以及String類型,Class類型

@NullSource: 表示為參數(shù)化測(cè)試提供一個(gè)null的入?yún)?/p>

@EnumSource: 表示為參數(shù)化測(cè)試提供一個(gè)枚舉入?yún)?/p>

@CsvSource:表示讀取CSV格式內(nèi)容作為參數(shù)化測(cè)試入?yún)?/p>

@CsvFileSource:表示讀取指定CSV文件內(nèi)容作為參數(shù)化測(cè)試入?yún)?/p>

@MethodSource:表示讀取指定方法的返回值作為參數(shù)化測(cè)試入?yún)?注意方法返回需要是一個(gè)流)

@ArgumentsSource:指定一個(gè)自定義的堂鲤,可重用的ArgumentsProvider

看完用法描述媒峡,簡直太喜歡了

一個(gè)頂三個(gè)基礎(chǔ)測(cè)試用例

@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("參數(shù)化測(cè)試1")
public void parameterizedTest1(String string) {
    assertTrue(StringUtils.isNotBlank(string));
}
image-20210416233807174

如果不是基礎(chǔ)的類型瘟栖,可以使用方法構(gòu)造,只要返回值為Stream類型就可以谅阿,多個(gè)參數(shù)使用Arguments實(shí)例流

@ParameterizedTest
@MethodSource("method")
@DisplayName("方法來源參數(shù)")
public void testWithExplicitLocalMethodSource(String name) {
    Assertions.assertNotNull(name);
}

private static Stream<String> method() {
    return Stream.of("apple", "banana");
}

@CsvSource允許您將參數(shù)列表表示為以逗號(hào)分隔的值(例如半哟,字符串文字)

@ParameterizedTest
@CsvSource({"steven,18", "jack,24"})
@DisplayName("參數(shù)化測(cè)試-csv格式")
public void parameterizedTest3(String name, Integer age) {
    System.out.println("name:" + name + ",age:" + age);
    Assertions.assertNotNull(name);
    Assertions.assertTrue(age > 0);
}
image-20210416232702304

@CsvFileSource使用classpath中的CSV文件酬滤,CSV文件中的每一行都會(huì)導(dǎo)致參數(shù)化測(cè)試的一次調(diào)用

這種就完全把測(cè)試數(shù)據(jù)與測(cè)試方法隔離,達(dá)到更好解耦效果

@ParameterizedTest
@CsvFileSource(resources = "/persons.csv")  //指定csv文件位置
@DisplayName("參數(shù)化測(cè)試-csv文件")
public void parameterizedTest2(String name, Integer age) {
    System.out.println("name:" + name + ",age:" + age);
    Assertions.assertNotNull(name);
    Assertions.assertTrue(age > 0);
}

其他方式不在贅述寓涨,如果還是滿足不了需求盯串,可以通過@ArgumentsSource自定義自己的數(shù)據(jù)來源,必須封裝成去取JSON或者XMl等數(shù)據(jù)

AssertJ

當(dāng)定義好需要運(yùn)行的測(cè)試方法后戒良,下一步則是需要關(guān)注測(cè)試方法的細(xì)節(jié)体捏,這就離不開斷言和假設(shè)

斷言:封裝好了常用判斷邏輯,當(dāng)不滿足條件時(shí)糯崎,該測(cè)試用例會(huì)被認(rèn)為測(cè)試失敗

假設(shè):與斷言類似几缭,當(dāng)條件不滿足時(shí),測(cè)試會(huì)直接退出而不是判定為失敗

因?yàn)椴粫?huì)影響到后續(xù)的測(cè)試用例沃呢,最常用的還是斷言

除了Junit5自帶的斷言奏司,AssertJ是非常好用的一個(gè)斷言工具,最大特點(diǎn)是提供了流式斷言樟插,與Java8使用方法非常類似

@Test
void testString() {
    // 斷言null或?yàn)榭兆址?    assertThat("").isNullOrEmpty();
    // 斷言空字符串
    assertThat("").isEmpty();
    // 斷言字符串相等 斷言忽略大小寫判斷字符串相等
    assertThat("niu").isEqualTo("niu").isEqualToIgnoringCase("NIu");
    // 斷言開始字符串 結(jié)束字符穿 字符串長度
    assertThat("niu").startsWith("ni").endsWith("u").hasSize(3);
    // 斷言包含字符串 不包含字符串
    assertThat("niu").contains("iu").doesNotContain("love");
    // 斷言字符串只出現(xiàn)過一次
    assertThat("niu").containsOnlyOnce("iu");
}

@Test
void testNumber() {
    // 斷言相等
    assertThat(42).isEqualTo(42);
    // 斷言大于 大于等于
    assertThat(42).isGreaterThan(38).isGreaterThanOrEqualTo(38);
    // 斷言小于 小于等于
    assertThat(42).isLessThan(58).isLessThanOrEqualTo(58);
    // 斷言0
    assertThat(0).isZero();
    // 斷言正數(shù) 非負(fù)數(shù)
    assertThat(1).isPositive().isNotNegative();
    // 斷言負(fù)數(shù) 非正數(shù)
    assertThat(-1).isNegative().isNotPositive();
}

@Test
void testCollection() {
    // 斷言 列表是空的
    assertThat(newArrayList()).isEmpty();
    // 斷言 列表的開始 結(jié)束元素
    assertThat(newArrayList(1, 2, 3)).startsWith(1).endsWith(3);
    // 斷言 列表包含元素 并且是排序的
    assertThat(newArrayList(1, 2, 3)).contains(1, atIndex(0)).contains(2, atIndex(1)).contains(3)
        .isSorted();
    // 斷言 被包含與給定列表
    assertThat(newArrayList(3, 1, 2)).isSubsetOf(newArrayList(1, 2, 3, 4));
    // 斷言 存在唯一元素
    assertThat(newArrayList("a", "b", "c")).containsOnlyOnce("a");
}

@Test
void testMap() {
    Map<String, Object> foo = ImmutableMap.of("A", 1, "B", 2, "C", 3);

    // 斷言 map 不為空 size
    assertThat(foo).isNotEmpty().hasSize(3);
    // 斷言 map 包含元素
    assertThat(foo).contains(entry("A", 1), entry("B", 2));
    // 斷言 map 包含key
    assertThat(foo).containsKeys("A", "B", "C");
    // 斷言 map 包含value
    assertThat(foo).containsValue(3);
}
// 其他斷言韵洋,請(qǐng)自行探索......

想想如果沒有使用AssertJ時(shí)我們是如何寫斷言的,是不是需要多個(gè)assert黄锤,很繁瑣

AssertJ的斷言代碼清爽很多搪缨,流式斷言充分利用了java8之后的匿名方法和stream類型的特點(diǎn),很好的對(duì)Junit斷言方法做了補(bǔ)充鸵熟。

參考

https://junit.org/junit5/docs/current/user-guide/#overview

https://assertj.github.io/doc/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末副编,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子流强,更是在濱河造成了極大的恐慌痹届,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件打月,死亡現(xiàn)場(chǎng)離奇詭異队腐,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)奏篙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門柴淘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人秘通,你說我怎么就攤上這事为严。” “怎么了肺稀?”我有些...
    開封第一講書人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵第股,是天一觀的道長。 經(jīng)常有香客問我话原,道長夕吻,這世上最難降的妖魔是什么诲锹? 我笑而不...
    開封第一講書人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮梭冠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘改备。我一直安慰自己控漠,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開白布悬钳。 她就那樣靜靜地躺著盐捷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪默勾。 梳的紋絲不亂的頭發(fā)上碉渡,一...
    開封第一講書人閱讀 49,079評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音母剥,去河邊找鬼滞诺。 笑死,一個(gè)胖子當(dāng)著我的面吹牛环疼,可吹牛的內(nèi)容都是我干的习霹。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼炫隶,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼淋叶!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起伪阶,我...
    開封第一講書人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤煞檩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后栅贴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體斟湃,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年檐薯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了桐早。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡厨剪,死狀恐怖哄酝,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情祷膳,我是刑警寧澤陶衅,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站直晨,受9級(jí)特大地震影響搀军,放射性物質(zhì)發(fā)生泄漏膨俐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一罩句、第九天 我趴在偏房一處隱蔽的房頂上張望焚刺。 院中可真熱鬧,春花似錦门烂、人聲如沸乳愉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蔓姚。三九已至,卻和暖如春慨丐,著一層夾襖步出監(jiān)牢的瞬間坡脐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來泰國打工房揭, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留备闲,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓捅暴,卻偏偏與公主長得像浅役,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子伶唯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容