JUnit5註解學習指引

自動化程式碼美學發表於2021-07-02

註解(Annotations)是JUnit的標誌性技術,本文就來對它的20個註解,以及元註解和組合註解進行學習。

20個註解

org.junit.jupiter.api包中定義了這些註解,它們分別是:

  • @Test 測試方法,可以直接執行。

  • @ParameterizedTest 引數化測試,比如:

    @ParameterizedTest
    @ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
    void palindromes(String candidate) {
        assertTrue(StringUtils.isPalindrome(candidate));
    }
    
  • @RepeatedTest 重複測試,比如:

    @RepeatedTest(10)
    void repeatedTest() {
        // ...
    }
    
  • @TestFactory 測試工廠,專門生成測試方法,比如:

    import org.junit.jupiter.api.DynamicTest;
    
    @TestFactory
    Collection<DynamicTest> dynamicTestsFromCollection() {
        return Arrays.asList(
            dynamicTest("1st dynamic test", () -> assertTrue(isPalindrome("madam"))),
            dynamicTest("2nd dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
        );
    }
    
  • @TestTemplate 測試模板,比如:

    final List<String> fruits = Arrays.asList("apple", "banana", "lemon");
    
    @TestTemplate
    @ExtendWith(MyTestTemplateInvocationContextProvider.class)
    void testTemplate(String fruit) {
        assertTrue(fruits.contains(fruit));
    }
    
    public class MyTestTemplateInvocationContextProvider
            implements TestTemplateInvocationContextProvider {
    
        @Override
        public boolean supportsTestTemplate(ExtensionContext context) {
            return true;
        }
    
        @Override
        public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
                ExtensionContext context) {
    
            return Stream.of(invocationContext("apple"), invocationContext("banana"));
        }
    }
    

    @TestTemplate必須註冊一個TestTemplateInvocationContextProvider,它的用法跟@Test類似。

  • @TestMethodOrder 指定測試順序,比如:

    import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
    import org.junit.jupiter.api.Order;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.TestMethodOrder;
    
    @TestMethodOrder(OrderAnnotation.class)
    class OrderedTestsDemo {
    
        @Test
        @Order(1)
        void nullValues() {
            // perform assertions against null values
        }
    
        @Test
        @Order(2)
        void emptyValues() {
            // perform assertions against empty values
        }
    
        @Test
        @Order(3)
        void validValues() {
            // perform assertions against valid values
        }
    
    }
    
  • @TestInstance 是否生成多個測試例項,預設JUnit每個測試方法生成一個例項,使用這個註解能讓每個類只生成一個例項,比如:

    @TestInstance(Lifecycle.PER_CLASS)
    class TestMethodDemo {
    
        @Test
        void test1() {
        }
    
        @Test
        void test2() {
        }
    
        @Test
        void test3() {
        }
    
    }
    
  • @DisplayName 自定義測試名字,會體現在測試報告中,比如:

    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() {
        }
    
    }
    
  • @DisplayNameGeneration 測試名字統一處理,比如:

    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) {
            }
    
        }
    
    }
    
  • @BeforeEach 在每個@Test, @RepeatedTest, @ParameterizedTest, or @TestFactory之前執行。

  • @AfterEach 在每個@Test, @RepeatedTest, @ParameterizedTest, or @TestFactory之後執行。

  • @BeforeAll所有的@Test, @RepeatedTest, @ParameterizedTest, and @TestFactory之前執行。

  • @AfterAll所有的@Test, @RepeatedTest, @ParameterizedTest, and @TestFactory之後執行。

  • @Nested 巢狀測試,一個類套一個類,例子參考上面那個。

  • @Tag 打標籤,相當於分組,比如:

    import org.junit.jupiter.api.Tag;
    import org.junit.jupiter.api.Test;
    
    @Tag("fast")
    @Tag("model")
    class TaggingDemo {
    
        @Test
        @Tag("taxes")
        void testingTaxCalculation() {
        }
    
    }
    
  • @Disabled 禁用測試,比如:

    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() {
        }
    
    }
    
  • @Timeout 對於test, test factory, test template, or lifecycle method,如果超時了就認為失敗了,比如:

    class TimeoutDemo {
    
        @BeforeEach
        @Timeout(5)
        void setUp() {
            // fails if execution time exceeds 5 seconds
        }
    
        @Test
        @Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
        void failsIfExecutionTimeExceeds100Milliseconds() {
            // fails if execution time exceeds 100 milliseconds
        }
    
    }
    
  • @ExtendWith 註冊擴充套件,比如:

    @ExtendWith(RandomParametersExtension.class)
    @Test
    void test(@Random int i) {
        // ...
    }
    

    JUnit5提供了標準的擴充套件機制來允許開發人員對JUnit5的功能進行增強。JUnit5提供了很多的標準擴充套件介面,第三方可以直接實現這些介面來提供自定義的行為。

  • @RegisterExtension 通過欄位註冊擴充套件,比如:

    class WebServerDemo {
    
        @RegisterExtension
        static WebServerExtension server = WebServerExtension.builder()
            .enableSecurity(false)
            .build();
    
        @Test
        void getProductList() {
            WebClient webClient = new WebClient();
            String serverUrl = server.getServerUrl();
            // Use WebClient to connect to web server using serverUrl and verify response
            assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus());
        }
    
    }
    
  • @TempDir 臨時目錄,比如:

    @Test
    void writeItemsToFile(@TempDir Path tempDir) throws IOException {
        Path file = tempDir.resolve("test.txt");
    
        new ListWriter(file).write("a", "b", "c");
    
        assertEquals(singletonList("a,b,c"), Files.readAllLines(file));
    }
    

元註解和組合註解

JUnit Jupiter支援元註解,能實現自定義註解,比如自定義@Fast註解:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.junit.jupiter.api.Tag;

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
public @interface Fast {
}

使用:

@Fast
@Test
void myFastTest() {
    // ...
}

這個@Fast註解也是組合註解,甚至可以更進一步和@Test組合:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
@Test
public @interface FastTest {
}

只用@FastTest就可以了:

@FastTest
void myFastTest() {
    // ...
}

小結

本文對JUnit20個主要的註解進行了介紹和示例演示,JUnit Jupiter支援元註解,可以自定義註解,也可以把多個註解組合起來。

參考資料:

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

https://vitzhou.gitbooks.io/junit5/content/junit/extension_model.html#概述

相關文章