JUnit 5 Annotations

Trong bài viết này ta cùng tìm hiểu về tất cả các Annotations mà JUnit 5 hỗ trợ. Toàn bộ các Annotations lõi của JUnit 5 đều nằm trong package org.junit.jupiter.api thuộc module junit-jupiter-api.

Giờ ta liệt kê toàn bộ danh sách các Annotations mà JUnit 5 hỗ trợ.

  • @Test
  • @ParameterizedTest
  • @RepeatedTest
  • @TestFactory
  • @TestInstance
  • @TestTemplate
  • @DisplayName
  • @BeforeEach
  • @AfterEach
  • @BeforeAll
  • @AfterAll
  • @Nested
  • @Tag
  • @Disabled
  • @ExtendWith Ta đi tỉm hiểu ý nghĩa từng annotation thông qua ví dụ cụ thể.

@Test

Annotation này biểu thị rằng method này là method test. Không giống với annotation @Test trong JUnit 4, anotaion này không chứa bất kì một attribute nào. Từ khi test mở rộng trong JUnit Jupiter hoạt động dựa trên các annotation của riêng nó. Các phương thức như vậy được kế thừa trừ khi nó bị ghi đè.

import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class FirstJUnit5Tests {
    @Test
    void myFirstTest() {
        assertEquals(2, 1 + 1);
    }
}

@ParameterizedTest

Annotation này cung cấp một khả năng lặp lại test một số lần xác định một cách đơn giản bằng cách trú thích một method băng annotation @ParameterizedTest và xác định tổng số lần lặp lại mong muốn. Mỗi lời gọi của phép lặp test hoạt động giống như việc thực hiện một method test @Test thông thường với sự hỗ trợ đầy đủ cho các callbacks và các phần mở rộng vòng đời giống nhau.

Ví dụ sau đây minh họa cách khai báo một method test có tên repeatTest() sẽ được lặp lại tự động 10 lần.

@RepeatedTest(10)
void repeatedTest() {
   // ...
}

@RepeatedTest(5)
void repeatedTestWithRepetitionInfo(RepetitionInfo repetitionInfo) {
    assertEquals(5, repetitionInfo.getTotalRepetitions());
}

@RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}")
@DisplayName("Repeat!")
void customDisplayName(TestInfo testInfo) {
    assertEquals(testInfo.getDisplayName(), "Repeat! 1/1");
}

@DisplayName

Test class và test method có thể định nghĩa tên hiển thị một cách tùy biến với khoảng trắng, kí tự đặc biệt, và emojis. Tên này sẽ được hiển thị trong test runners và test report.

Tùy biến hiển thị tên class.

@DisplayName("MyTestClass")
public class DisplayNameTest {
}

Tùy biến hiển thị tên method.

@Test
@DisplayName("Example Test Method with No Business Logic")
void test() {
	assertTrue(3 > 0);
}

Hiển thị tên method với emojis.

@Test
@DisplayName("MyTestMethod ☺")
void test1(TestInfo testInfo) {
	assertEquals("MyTestMethod ☺", testInfo.getDisplayName());
}

Giờ tên tùy biến được hiển thị trong test report như sau.

@BeforeEach

Annotation này biểu thị rằng phương thức được chú thích phải được thực hiện trước mỗi phương thức @Test, @RepeatedTest, @ParameterizedTest hoặc @TestFactory trong lớp hiện tại. Tương tự với @Before của JUnit 4. Các phương thức như vậy được kế thừa trừ khi chúng bị ghi đè.

class StandardTests {
    @BeforeEach
    void init() {
    }
    @Test
    void succeedingTest() {
    }
    @AfterEach
    void tearDown() {
    }
}

@AfterEach

Annotation này biểu thị rằng phương thức được chú thích nên được thực hiện sau mỗi phương thức @Test, @RepeatedTest, @ParameterizedTest hoặc @TestFactory trong lớp hiện tại. Tương tự như @After của JUnit 4. Các phương thức như vậy được kế thừa trừ khi chúng bị ghi đè.

class StandardTests {
    @BeforeEach
    void init() {
    }
    @Test
    void succeedingTest() {
    }
    @AfterEach
    void tearDown() {
    }
}

@BeforAll

Annotation này biểu thị rằng phương thức được chú thích phải được thực hiện trước tất cả các phương thức @Test, @RepeatedTest, @ParameterizedTest hoặc @TestFactory trong lớp hiện tại. Tương tự như @BeforeClass của JUnit 4.

class StandardTests {
    @BeforeAll
    static void initAll() {
    }
    @Test
    void succeedingTest() {
    }
    @AfterAll
    static void tearDownAll() {
    }
}

@AfterAll

Annotation này biểu thị rằng phương thức được chú thích phải được thực hiện sau tất cả các phương thức @Test, @RepeatedTest, @ParameterizedTest hoặc @TestFactory trong lớp hiện tại. Tương tự như @AfterClass của JUnit 4.

class StandardTests {
    @AfterAll
    static void freeAll() {
    }
    @Test
    void succeedingTest() {
    }
    @AfterAll
    static void tearDownAll() {
    }
}

@Nested

Annotation này có nghĩa là THỬ NGHIỆM, nó biểu thị rằng phương thức được chú thích phải được thực hiện trước tất cả @Test, @RepeatedTest, @Parameterize. Annotaion này biểu thị rằng lớp được chú thích là lớp thử nghiệm không lồng nhau, không tĩnh (non-static). Các phương thức @BeforeAll@AfterAll không thể được sử dụng trực tiếp trong lớp thử nghiệm @Nested trừ khi vòng đời cá thể kiểm tra "per-class" được sử dụng. Chú thích như vậy không được kế thừa.

Các thử nghiệm lồng nhau cho phép người viết thử nghiệm có thêm khả năng thể hiện mối quan hệ giữa một số nhóm thử nghiệm. Đây là một ví dụ phức tạp.

Bộ thử nghiệm lồng nhau để kiểm tra ngăn xếp: phương thức @Test@TestFactory trong lớp hiện tại. Tương tự với @BeforeClass của JUnit 4.

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.EmptyStackException;
import java.util.Stack;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

@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());
        }
        
        @Test
        @DisplayName("throws EmptyStackException when popped")
        void throwsExceptionWhenPopped() {
            assertThrows(EmptyStackException.class, () -> stack.pop());
        }
        
        @Test
        @DisplayName("throws EmptyStackException when peeked")
        void throwsExceptionWhenPeeked() {
            assertThrows(EmptyStackException.class, () -> stack.peek());
        }
        
        @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());
            }
            
            @Test
            @DisplayName("returns the element when popped and is empty")
            void returnElementWhenPopped() {
                assertEquals(anElement, stack.pop());
                assertTrue(stack.isEmpty());
            }
            
            @Test
            @DisplayName("returns the element when peeked but remains not empty")
            void returnElementWhenPeeked() {
                assertEquals(anElement, stack.peek());
                assertFalse(stack.isEmpty());
            }
        }
    }
}

@Tag

Annotation này được sử dụng để khai báo các thẻ cho việc kiểm tra lọc, ở cấp class hoặc method. Tương tự như test nhóm thử nghiệm trong TestNG hoặc Categories trong JUnit 4. Chú thích như vậy được kế thừa ở cấp class chứ không phải ở cấp method.

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

@Tag("fast")
@Tag("model")
class TaggingDemo {

    @Test
    @Tag("taxes")
    void testingTaxCalculation() {
    }
}

@Disabled

Annotation này dùng để vô hiệu hóa một class hay method test, nó tương tự như @Ignore của JUnit 4. Annotation này không được phép kế thừa.

Vô hiệu hóa class test.

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@Disabled
class DisabledClassDemo {
    @Test
    void testWillBeSkipped() {
    }
}

Vô hiệu hóa method test.

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
class DisabledTestsDemo {
    @Disabled
    @Test
    void testWillBeSkipped() {
    }
    
    @Test
    void testWillBeExecuted() {
    }
}

@ExtendWith

Annotation này được dùng để đăng ký các mở rộng tùy biến. Ví dụ để đăng ký một tùy biến RandomParametersExtension cho một method cụ thể, chúng ta chú thích method test như sau.

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

@TestFactory

Test động được tạo ra khi chạy - runtime bởi method Factory được chú thích bằng @TestFactory. Lớp DynamicTestsDemo sau đây trình bày một số ví dụ về các test Factory và các test động.

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.function.ThrowingConsumer;
class DynamicTestsDemo {
    // This will result in a JUnitException!
    @TestFactory
    List<String> dynamicTestsWithInvalidReturnType() {
        return Arrays.asList("Hello");
    }
    @TestFactory
    Collection<DynamicTest> dynamicTestsFromCollection() {
        return Arrays.asList(
            dynamicTest("1st dynamic test", () -> assertTrue(true)),
            dynamicTest("2nd dynamic test", () -> assertEquals(4, 2 * 2))
        );
    }
    @TestFactory
    Iterable<DynamicTest> dynamicTestsFromIterable() {
        return Arrays.asList(
            dynamicTest("3rd dynamic test", () -> assertTrue(true)),
            dynamicTest("4th dynamic test", () -> assertEquals(4, 2 * 2))
        );
    }
    @TestFactory
    Iterator<DynamicTest> dynamicTestsFromIterator() {
        return Arrays.asList(
            dynamicTest("5th dynamic test", () -> assertTrue(true)),
            dynamicTest("6th dynamic test", () -> assertEquals(4, 2 * 2))
        ).iterator();
    }
    @TestFactory
    Stream<DynamicTest> dynamicTestsFromStream() {
        return Stream.of("A", "B", "C")
            .map(str -> dynamicTest("test" + str, () -> { /* ... */ }));
    }
    @TestFactory
    Stream<DynamicTest> dynamicTestsFromIntStream() {
        // Generates tests for the first 10 even integers.
        return IntStream.iterate(0, n -> n + 2).limit(10)
            .mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0)));
    }
    @TestFactory
    Stream<DynamicTest> generateRandomNumberOfTests() {
        // Generates random positive integers between 0 and 100 until
        // a number evenly divisible by 7 is encountered.
        Iterator<Integer> inputGenerator = new Iterator<Integer>() {
            Random random = new Random();
            int current;
            @Override
            public boolean hasNext() {
                current = random.nextInt(100);
                return current % 7 != 0;
            }
            @Override
            public Integer next() {
                return current;
            }
        };
        // Generates display names like: input:5, input:37, input:85, etc.
        Function<Integer, String> displayNameGenerator = (input) -> "input:" + input;
        // Executes tests based on the current input value.
        ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0);
        // Returns a stream of dynamic tests.
        return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
    }
    @TestFactory
    Stream<DynamicNode> dynamicTestsWithContainers() {
        return Stream.of("A", "B", "C")
            .map(input -> dynamicContainer("Container " + input, Stream.of(
                dynamicTest("not null", () -> assertNotNull(input)),
                dynamicContainer("properties", Stream.of(
                    dynamicTest("length > 0", () -> assertTrue(input.length() > 0)),
                    dynamicTest("not empty", () -> assertFalse(input.isEmpty()))
                ))
            )));
    }
}

@TestInstance

Annotation này dùng để cấu hình vòng đời cho một đối tượng test cụ thể. Ví dụ, nếu bạn muốn JUnit Jupiter thực hiện tất cả các phương thức test trên cùng một đối tượng test cụ thể, chỉ cần chú thích class test của bạn với @TestInstance (Lifecycle.PER_CLASS).

@TestTemplate

Một phương thức @TestTemplate không phải là trường hợp test thông thường mà là mẫu cho các trường hợp test. Như vậy, nó được thiết kế để được gọi nhiều lần tùy thuộc vào số lượng ngữ cảnh yêu cầu được trả về bởi các provider đã đăng ký. Do đó, nó phải được sử dụng kết hợp với phần mở rộng TestTemplateInvocationContextProvider đã đăng ký.

@TestTemplate
@ExtendWith(MyTestTemplateInvocationContextProvider.class)
void testTemplate(String parameter) {
    assertEquals(3, parameter.length());
}

public class MyTestTemplateInvocationContextProvider implements TestTemplateInvocationContextProvider {
    @Override
    public boolean supportsTestTemplate(ExtensionContext context) {
        return true;
    }
    @Override
    public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext context) {
        return Stream.of(invocationContext("foo"), invocationContext("bar"));
    }
    private TestTemplateInvocationContext invocationContext(String parameter) {
        return new TestTemplateInvocationContext() {
            @Override
            public String getDisplayName(int invocationIndex) {
                return parameter;
            }
            @Override
            public List<Extension> getAdditionalExtensions() {
                return Collections.singletonList(new ParameterResolver() {
                    @Override
                    public boolean supportsParameter(ParameterContext parameterContext,
                            ExtensionContext extensionContext) {
                        return parameterContext.getParameter().getType().equals(String.class);
                    }
                    @Override
                    public Object resolveParameter(ParameterContext parameterContext,
                            ExtensionContext extensionContext) {
                        return parameter;
                    }
                });
            }
        };
    }
}

Đến đây chúng ta đã hiểu được ý nghĩa của toàn bộ các annotation trong JUnit 5 thông qua những ví dụ cụ thể.

Tài liệu tham khảo.