JUnit5的測試不是通過名稱,而是通過註解來標識的。

測試類與方法

Test Class:測試類,必須包含至少一個test方法,包括:

  • 最外層的class
  • static member class
  • @Nested class

Test Method:測試方法,包括:

  • @Test
  • @RepeatedTest
  • @ParameterizedTest
  • @TestFactory
  • @TestTemplate

Lifecycle Method:生命週期方法,包括:

  • @BeforeAll
  • @AfterAll
  • @BeforeEach
  • @AfterEach

注意:

  1. Test Method和Lifecycle Method不能是abstract也不能return。它們可以在當前測試類中宣告,也可以繼承自父類或介面。
  2. Test class、Test Method和Lifecycle Method都不能是private。

示例程式碼:

import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue; import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; class StandardTests { @BeforeAll
static void initAll() {
} @BeforeEach
void init() {
} @Test
void succeedingTest() {
} @Test
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");
} @AfterEach
void tearDown() {
} @AfterAll
static void tearDownAll() {
} }

自定義顯示名字

Test class和test method可以使用@DisplayName自定義在測試報告中的顯示名字,支援空格、特殊字元和emoji表情符號。

示例:

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; @DisplayName("A special test case")
class DisplayNameDemo { @Test
@DisplayName("Custom test name containing spaces")
void testWithDisplayNameContainingSpaces() {
} @Test
@DisplayName("╯°□°)╯")
void testWithDisplayNameContainingSpecialCharacters() {
} @Test
@DisplayName("")
void testWithDisplayNameContainingEmoji() {
} }

除了@DisplayName@DisplayNameGeneration 註解能用來對顯示名字做統一處理,JUnit Jupiter自帶了一些生成器:

  • Standard 匹配標準行為
  • Simple 刪除沒有引數的方法後面的括號
  • ReplaceUnderscores 用空格替換下劃線
  • IndicativeSentences 把test class和test method名字連線起來

示例程式碼:

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.IndicativeSentencesGeneration;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource; class DisplayNameGeneratorDemo { @Nested
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class A_year_is_not_supported { @Test
void if_it_is_zero() {
} @DisplayName("A negative value for year is not supported by the leap year computation.")
@ParameterizedTest(name = "For example, year {0} is not supported.")
@ValueSource(ints = { -1, -4 })
void if_it_is_negative(int year) {
} } @Nested
@IndicativeSentencesGeneration(separator = " -> ", generator = DisplayNameGenerator.ReplaceUnderscores.class)
class A_year_is_a_leap_year { @Test
void if_it_is_divisible_by_4_but_not_by_100() {
} @ParameterizedTest(name = "Year {0} is a leap year.")
@ValueSource(ints = { 2016, 2020, 2048 })
void if_it_is_one_of_the_following_years(int year) {
} } }

@IndicativeSentencesGeneration可以自定義separator和generator。

結果:

+-- DisplayNameGeneratorDemo [OK]
+-- A year is not supported [OK]
| +-- A negative value for year is not supported by the leap year computation. [OK]
| | +-- For example, year -1 is not supported. [OK]
| | '-- For example, year -4 is not supported. [OK]
| '-- if it is zero() [OK]
'-- A year is a leap year [OK]
+-- A year is a leap year -> if it is divisible by 4 but not by 100. [OK]
'-- A year is a leap year -> if it is one of the following years. [OK]
+-- Year 2016 is a leap year. [OK]
+-- Year 2020 is a leap year. [OK]
'-- Year 2048 is a leap year. [OK]

除了註解,也能通過配置設定全域性的預設Generator,比如在 src/test/resources/junit-platform.properties檔案中:

junit.jupiter.displayname.generator.default = \
org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores

既可以指定現有Generator,也可以指定實現了DisplayNameGenerator介面的類。

自定義顯示名字的優先順序是:

  1. @DisplayName
  2. @DisplayNameGeneration
  3. junit.jupiter.displayname.generator.default
  4. org.junit.jupiter.api.DisplayNameGenerator.Standard

斷言(Assertions)

JUnit5的斷言是包含在org.junit.jupiter.api.Assertions中的靜態方法,比如assertTrue、assertEquals、assertNotNull、assertAll、assertThrows、assertTimeout、assertTimeoutPreemptively等。

示例程式碼如下:

import static java.time.Duration.ofMillis;
import static java.time.Duration.ofMinutes;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTimeout;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.concurrent.CountDownLatch; import example.domain.Person;
import example.util.Calculator; import org.junit.jupiter.api.Test; class AssertionsDemo { private final Calculator calculator = new Calculator(); private final Person person = new Person("Jane", "Doe"); @Test
void standardAssertions() {
assertEquals(2, calculator.add(1, 1));
assertEquals(4, calculator.multiply(2, 2),
"The optional failure message is now the last parameter");
assertTrue('a' < 'b', () -> "Assertion messages can be lazily evaluated -- "
+ "to avoid constructing complex messages unnecessarily.");
} @Test
void groupedAssertions() {
// In a grouped assertion all assertions are executed, and all
// failures will be reported together.
assertAll("person",
() -> assertEquals("Jane", person.getFirstName()),
() -> assertEquals("Doe", person.getLastName())
);
} @Test
void dependentAssertions() {
// Within a code block, if an assertion fails the
// subsequent code in the same block will be skipped.
assertAll("properties",
() -> {
String firstName = person.getFirstName();
assertNotNull(firstName); // Executed only if the previous assertion is valid.
assertAll("first name",
() -> assertTrue(firstName.startsWith("J")),
() -> assertTrue(firstName.endsWith("e"))
);
},
() -> {
// Grouped assertion, so processed independently
// of results of first name assertions.
String lastName = person.getLastName();
assertNotNull(lastName); // Executed only if the previous assertion is valid.
assertAll("last name",
() -> assertTrue(lastName.startsWith("D")),
() -> assertTrue(lastName.endsWith("e"))
);
}
);
} @Test
void exceptionTesting() {
Exception exception = assertThrows(ArithmeticException.class, () ->
calculator.divide(1, 0));
assertEquals("/ by zero", exception.getMessage());
} @Test
void timeoutNotExceeded() {
// The following assertion succeeds.
assertTimeout(ofMinutes(2), () -> {
// Perform task that takes less than 2 minutes.
});
} @Test
void timeoutNotExceededWithResult() {
// The following assertion succeeds, and returns the supplied object.
String actualResult = assertTimeout(ofMinutes(2), () -> {
return "a result";
});
assertEquals("a result", actualResult);
} @Test
void timeoutNotExceededWithMethod() {
// The following assertion invokes a method reference and returns an object.
String actualGreeting = assertTimeout(ofMinutes(2), AssertionsDemo::greeting);
assertEquals("Hello, World!", actualGreeting);
} @Test
void timeoutExceeded() {
// The following assertion fails with an error message similar to:
// execution exceeded timeout of 10 ms by 91 ms
assertTimeout(ofMillis(10), () -> {
// Simulate task that takes more than 10 ms.
Thread.sleep(100);
});
} @Test
void timeoutExceededWithPreemptiveTermination() {
// The following assertion fails with an error message similar to:
// execution timed out after 10 ms
assertTimeoutPreemptively(ofMillis(10), () -> {
// Simulate task that takes more than 10 ms.
new CountDownLatch(1).await();
});
} private static String greeting() {
return "Hello, World!";
} }

假設(Assumptions)

JUnit5的斷言是包含在org.junit.jupiter.api.Assumptions中的靜態方法,比如assumeTrue、assumingThat等。

示例程式碼:

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.Assumptions.assumingThat; import example.util.Calculator; import org.junit.jupiter.api.Test; class AssumptionsDemo { private final Calculator calculator = new Calculator(); @Test
void testOnlyOnCiServer() {
assumeTrue("CI".equals(System.getenv("ENV")));
// remainder of test
} @Test
void testOnlyOnDeveloperWorkstation() {
assumeTrue("DEV".equals(System.getenv("ENV")),
() -> "Aborting test: not on developer workstation");
// remainder of test
} @Test
void testInAllEnvironments() {
assumingThat("CI".equals(System.getenv("ENV")),
() -> {
// perform these assertions only on the CI server
assertEquals(2, calculator.divide(4, 2));
}); // perform these assertions in all environments
assertEquals(42, calculator.multiply(6, 7));
} }

Assertions與Assumptions區別

Assertions如果失敗,test會被標記為failed。Assumptions如果失敗,test會被標記為ignored,測試不會執行

示例:

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue; public class Demo {
@Test
void assertTest() {
assertTrue(false);
} @Test
void assumeTest() {
assumeTrue(false);
}
}

結果:

禁用測試

@Disabled能用來禁用test class或test method,建議在括號內填寫上禁用理由。

示例:

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; @Disabled("Disabled until bug #99 has been fixed")
class DisabledClassDemo { @Test
void testWillBeSkipped() {
} }
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; class DisabledTestsDemo { @Disabled("Disabled until bug #42 has been resolved")
@Test
void testWillBeSkipped() {
} @Test
void testWillBeExecuted() {
} }

小結

本文首先介紹瞭如何使用測試類與方法,來編寫一個JUnit5的基本測試,然後介紹瞭如何自定義測試報告中的顯示名字。使用斷言(Assertions)可以把test標記為failed,使用假設(Assumptions)可以把test標記為ignored。最後介紹瞭如何禁用測試。除了基本測試,JUnit5還能編寫帶條件的測試。

參考資料:

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

https://stackoverflow.com/questions/44628483/assume-vs-assert-in-junit-tests