五年了,你還在用Junit4嗎?


junit5

JUnit5在2017年就發布了,你還在用junit4嗎?

什么是junit5

與以前的JUnit版本不同,JUnit 5由三個不同子項目的多個不同模塊組成。

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

JUnit Platform為在JVM上啟動測試框架提供基礎。它還定義了TestEngine API, 用來開發在平台上運行的測試框架。此外,平台提供了一個控制台啟動器],用於從命令行啟動平台,並為Gradle和Maven提供構建插件以[基於JUnit 4的Runner,用於在平台上運行任意TestEngine

JUnit Jupiter是在JUnit 5中編寫測試和擴展的新型編程模型和[擴展模型][]的組合.Jupiter子項目提供了TestEngine,用於在平台上運行基於Jupiter的測試。

JUnit Vintage提供TestEngine,用於在平台上運行基於JUnit 3和JUnit 4的測試。

為什么需要 JUnit 5

自從有了類似 JUnit 之類的測試框架,Java 單元測試領域逐漸成熟,開發人員對單元測試框架也有了更高的要求:更多的測試方式,更少的其他庫的依賴。

因此,大家期待着一個更強大的測試框架誕生,JUnit 作為Java測試領域的領頭羊,推出了 JUnit 5 這個版本,主要特性:

  • 提供全新的斷言和測試注解,支持測試類內嵌
  • 更豐富的測試方式:支持動態測試,重復測試,參數化測試等
  • 實現了模塊化,讓測試執行和測試發現等不同模塊解耦,減少依賴
  • 提供對 Java 8 的支持,如 Lambda 表達式,Sream API等。

基本注解

@Test: 表示方法是測試方法。但是與JUnit4的@Test不同,他的職責非常單一不能聲明任何屬性,拓展的測試將會由Jupiter提供額外測試

@ParameterizedTest: 表示方法是參數化測試

@RepeatedTest: 表示方法可重復執行

@DisplayName: 為測試類或者測試方法設置展示名稱

@BeforeEach: 表示在每個單元測試之前執行

@AfterEach: 表示在每個單元測試之后執行

@BeforeAll: 表示在所有單元測試之前執行

@AfterAll: 表示在所有單元測試之后執行

@Tag: 表示單元測試類別,類似於JUnit4中的@Categories

@Disabled: 表示測試類或測試方法不執行,類似於JUnit4中的@Ignore

@Timeout: 表示測試方法運行如果超過了指定時間將會返回錯誤

@ExtendWith: 為測試類或測試方法提供擴展類引用

常用注解格式:

class StandardTests {

    //與junit4的@beforeClass類似,每個測試類運行一次
    @BeforeAll
    static void initAll() {
    }

    //與junit4中@before類似,每個測試用例都運行一次
    @BeforeEach
    void init() {
    }

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

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

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

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


    //與@BeforeEach對應,每個測試類執行一次,一般用於恢復環境
    @AfterEach
    void tearDown() {
    }

    //與@BeforeAll對應,每個測試類執行一次,一般用於恢復環境
    @AfterAll
    static void tearDownAll() {
    }
}

新特性

顯示名稱

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

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

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

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

IDE運行測試結果顯示:

image-20210416232329161

優點:通過這種方式,可以在方法名是英文特別長或者很難用英文描述清楚的場景下,增加中文解釋

更強大的斷言

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

分組斷言:

多個條件同時滿足時才斷言成功

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

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

異常斷言:

Junit4時需要使用rule方式,junit5提供了assertThrows更優雅的異常斷言

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

超時斷言:

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

標簽和過濾

通過標簽把測試分組,在不同階段執行不同的邏輯測試,比如划分為快速冒煙測試和執行慢但也重要的測試

@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>

嵌套測試

當我們編寫的類和代碼逐漸增多,隨之而來的需要測試的對應測試類也會越來越多。

為了解決測試類數量爆炸的問題,JUnit 5提供了@Nested 注解,能夠以靜態內部成員類的形式對測試用例類進行邏輯分組。

並且每個靜態內部類都可以有自己的生命周期方法, 這些方法將按從外到內層次順序執行。

此外,嵌套的類也可以用@DisplayName 標記,這樣我們就可以使用正確的測試名稱。下面看下簡單的用法:

@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沒有限制嵌套層數,除非必要一般不建議使用超過3層,過於復雜的層次結構會增加開發者理解用例關系的難度

構造函數和方法的依賴注入

在之前的所有JUnit版本中,測試構造函數或方法都不允許有參數(至少不能使用標准的Runner實現)。作為JUnit Jupiter的主要變化之一,測試構造函數和方法現在都允許有參數。這帶來了更大的靈活性,並為構造函數和方法啟用依賴注入

  • TestInfo可獲取測試信息
  • TestReporter可以向控制台輸出信息
@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");
}

重復測試

多次調用同一個測試用例

@RepeatedTest(10)
@DisplayName("重復測試")
public void testRepeated() {
    //...
}

image-20210416232512919

動態測試

動態測試只需要編寫一處代碼,就能一次性對各種類型的輸入和輸出結果進行驗證

@TestFactory
@DisplayName("動態測試")
Stream<DynamicTest> dynamicTests() {
    List<Person> persons = getAllPerson();

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

超時測試

通過時間來驗證用例是否超時,一般要求單個單元測試不應該超過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));
    }
}

參數測試

參數測試我覺得是最好用的特性,可以大量減少重復模板式代碼,也是junit5最驚艷的提升,強烈推薦使用

@ValueSource: 為參數化測試指定入參來源,支持八大基礎類以及String類型,Class類型

@NullSource: 表示為參數化測試提供一個null的入參

@EnumSource: 表示為參數化測試提供一個枚舉入參

@CsvSource:表示讀取CSV格式內容作為參數化測試入參

@CsvFileSource:表示讀取指定CSV文件內容作為參數化測試入參

@MethodSource:表示讀取指定方法的返回值作為參數化測試入參(注意方法返回需要是一個流)

@ArgumentsSource:指定一個自定義的,可重用的ArgumentsProvider

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

一個頂三個基礎測試用例

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

image-20210416233807174

如果不是基礎的類型,可以使用方法構造,只要返回值為Stream類型就可以,多個參數使用Arguments實例流

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

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

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

@ParameterizedTest
@CsvSource({"steven,18", "jack,24"})
@DisplayName("參數化測試-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文件中的每一行都會導致參數化測試的一次調用

這種就完全把測試數據與測試方法隔離,達到更好解耦效果

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

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

AssertJ

當定義好需要運行的測試方法后,下一步則是需要關注測試方法的細節,這就離不開斷言和假設

斷言:封裝好了常用判斷邏輯,當不滿足條件時,該測試用例會被認為測試失敗

假設:與斷言類似,當條件不滿足時,測試會直接退出而不是判定為失敗

因為不會影響到后續的測試用例,最常用的還是斷言

除了Junit5自帶的斷言,AssertJ是非常好用的一個斷言工具,最大特點是提供了流式斷言,與Java8使用方法非常類似

@Test
void testString() {
    // 斷言null或為空字符串
    assertThat("").isNullOrEmpty();
    // 斷言空字符串
    assertThat("").isEmpty();
    // 斷言字符串相等 斷言忽略大小寫判斷字符串相等
    assertThat("niu").isEqualTo("niu").isEqualToIgnoringCase("NIu");
    // 斷言開始字符串 結束字符穿 字符串長度
    assertThat("niu").startsWith("ni").endsWith("u").hasSize(3);
    // 斷言包含字符串 不包含字符串
    assertThat("niu").contains("iu").doesNotContain("love");
    // 斷言字符串只出現過一次
    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();
    // 斷言正數 非負數
    assertThat(1).isPositive().isNotNegative();
    // 斷言負數 非正數
    assertThat(-1).isNegative().isNotPositive();
}

@Test
void testCollection() {
    // 斷言 列表是空的
    assertThat(newArrayList()).isEmpty();
    // 斷言 列表的開始 結束元素
    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);
}
// 其他斷言,請自行探索......

想想如果沒有使用AssertJ時我們是如何寫斷言的,是不是需要多個assert,很繁瑣

AssertJ的斷言代碼清爽很多,流式斷言充分利用了java8之后的匿名方法和stream類型的特點,很好的對Junit斷言方法做了補充。

參考

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM