Tags:

Parameterized trong JUnit 5

@Parameterized cho phép chúng ta thực thi một test method nhiều lần với các tham số khác nhau. Điều này sẽ rất tiện dụng trong khi chúng ta phải dùng vòng lặp hoặc gọi tường minh nhiều lần một test method thì giờ đây đã có Parameterized giải quyết. 

Trước tiên, để sử dụng Parameterized trong JUnit 5 chúng ta phải thêm dependency junit-jupiter-params vào dự án maven.

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.4.2</version>
    <scope>test</scope>
</dependency>

Giả sử mình có Numbers class vừa mới được triển khai cần được kiểm thử.

public class Numbers {
    public static boolean isOdd(int number) {
        return number % 2 != 0;
    }
}

Để kiểm thử isOdd() method mình cần phải sử dụng nhiều dữ liệu với các gía trị khác nhau để chắc rằng method hoạt động tốt với các kiểu dữ liệu.

@ParameterizedTest
@ValueSource(ints = {1, 3, 5, -3, 15, Integer.MAX_VALUE}) // six numbers
void isOddTest(int number) {
    assertTrue(Numbers.isOdd(number));
}

Kết quả là isOddTest() sẽ thực thi 6 lần, mỗi lần tương ứng với từng giá trị trong mảng ints được liệt kê trong @ValueSource.

Tham số đầu vào trong Parameterized

Parameterized cho phép thực thi 1 test method nhiều lần với các tham số khác nhau được liệt kê trong tham số đầu vào vởi @ValueSource. Thế nhưng @ValueSource cũng có một số giới hạn nhất định:

  • Chỉ hỗ trợ các kiểu dữ liệu nguyên thuỷ, StringClass
  • Chúng ta cũng chỉ có thể truyền các giá trị tham số đầu vào cho các test method chỉ có 1 đối số.
  • Chúng ta không thể truyền giá trị null trong @ValueSource kể cả kiểu String cũng không được phép.

Lưu ý @Valuesource chỉ hỗ trợ các tham số nhất định đối với từng loại dữ liệu:

  • ints – @ValueSource(ints = {1, 2, 3})
  • bytes – @ValueSource(bytes = {1, 2, 3})
  • shorts – @ValueSource(shorts = {1, 2, 3})
  • longs – @ValueSource(longs = {1, 2, 3}
  • floats – @ValueSource(floats = {1.4, 2.3, 3.6})
  • doubles – @ValueSource(doubles = {1,.234,  2.213, 3.343})
  • chars – @ValueSource(chars = {‘a’, ‘b’, ‘c’})
  • strings – @ValueSource(strings = {“test 1”, “test 2”})
  • classes – @ValueSource(classes = {Boolean.class, Number.class})

Giá trị Null và Empty 

Với các hạn chế trên thì JUnit 5 cho phép chúng ta truyền 1 tham số null vào test method với @NullSource.

@ParameterizedTest
@NullSource
void isNullTest(String input) {
    assertNull(input);
}

Chúng ta cũng có thể truyền một giá trị String rỗng với @EmptySource.

@ParameterizedTest
@EmptySource
void isEmptyTest(String input) {
    assertEquals(input, "");
}

Ngoài ra, @EmptySource cũng hoạt động với mảng, Collection.

@ParameterizedTest
@EmptySource
void isEmptyTest(Integer[] input) {
     assertEquals(0, input.length);
}

@ParameterizedTest
@EmptySource
void isEmptyTest(List<String> input) {
    assertEquals(0, input.size());
}

 @ParameterizedTest
 @EmptySource
 void isEmptyTest(Set<String> input) {
     assertEquals(0, input.size());
}

@ParameterizedTest
@EmptySource
void isEmptyTest(Map<String, Integer> input) {
    assertEquals(0, input.size());
}

Để nhanh chóng, chúng ta có thể kết hợp giá trị nullempty nếu cần thiết thông qua @NullAndEmptySource.

@ParameterizedTest
@NullAndEmptySource
void isBlankTest(String input) {
    assertTrue(input == null || input == "");
}

Enum Parameter

Nếu tham số là một Enum, chúng ta có thể sử dụng @EnumSource để truyền tất các giá trị của Enum class.

@ParameterizedTest
@EnumSource(Month.class)
void enumExample(Month month) {
    int monthNumber = month.getValue();
    assertTrue(monthNumber >= 1 && monthNumber <= 12);
}

Chúng ta cũng có thể chỉ định một số Enum thay vì chọn tất cả với names trong @EnumSource. Ví dụ mình sẽ kiểm tra tháng 4, 6, 9, 11 có 30 ngày. Lưu ý các giá trị chuỗi trong names phải giống với các Enum trong Month Enum.

@ParameterizedTest
@EnumSource(value = Month.class, names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"})
void someMonths_Are30DaysLong(Month month) {
    final boolean isALeapYear = false;
    assertEquals(30, month.length(isALeapYear));
}

Ngoài việc chọn các Enum được sử dụng cho test method thì chúng ta cũng có thể loại trừ các giá trị Enum với EnumSource.Mode.EXCLUDE.

@ParameterizedTest
@EnumSource(
        value = Month.class,
        names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER", "FEBRUARY"},
        mode = EnumSource.Mode.EXCLUDE)
void exceptFourMonths_OthersAre31DaysLong(Month month) {
    final boolean isALeapYear = false;
    assertEquals(31, month.length(isALeapYear));
}

Chúng ta cũng có thể sử dụng java regex để tìm kiếm các giá trị tương ứng với chuỗi regex đã cho.

@ParameterizedTest
@EnumSource(value = Month.class, names = ".*BER", mode = EnumSource.Mode.MATCH_ANY)
void fourMonths_AreEndingWithBer(Month month) {
    EnumSet<Month> months =
            EnumSet.of(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER, Month.DECEMBER);
    assertTrue(months.contains(month));
}

Tạo nhiều tham số đầu vào cho test method

Ở trên chúng ta chỉ đang thao tác với các test method chứa duy nhất một tham số đầu vào, vậy đối với các tham số đầu vào thì sao? Trong phần này chúng ta sẽ sử dụng Static Factory method để chuẩn bị data cho test method chứa nhiều tham số đầu vào, đồng thời chúng cũng có thể sử dụng cho nhiều test method khác miễn là chúng có cùng các tham số đầu vào.

Tuy nhiên khi sử dụng Factory method để chuẩn bị dữ liệu chúng ta cần lưu ý một vài điều sau:

  • Factory method không được chứa các tham số đầu vào.
  • Factory method phải là static method.
  • Factory method phải return về 1 Stream, Iterable. Iterator, hoặc mảng các tham số.
  • Các giá trị trả về trong Factory method phải tương ứng với vị trí và kiểu dữ liệu trong test method
  • Chúng ta có sử dụng Factory method với @MethodSource nhận một tham số đầu vào là tên của factory method.

Ví dụ đối với test method testParameterizedTest() sử dụng Factory method sumProvider().

@DisplayName("Should calculate the correct sum")
@ParameterizedTest(name = "{index} => a={0}, b={1}, sum={2}")
@MethodSource("sumProvider")
void testParameterizedTest(int a, int b, int sum, String message) {
    assertEquals(sum, a + b);
    assertTrue(message != null && message != "");
}

Chúng ta có thể tạo Factory method với Stream như sau

private static Stream<Arguments> sumProvider() {
    return Stream.of(
            Arguments.of(1, 1, 2, "message 1"),
            Arguments.of(2, 3, 5, , "message 2")
    );
}

Hoặc với mảng 2 chiều.

private static Object[][] sumProvider() {
    return new Object[][]{{1, 1, 2, "message 1"}, {2, 3, 5, "message 2"}};
}

Nếu @MethodSource không được cung cấp tên method của Factory method, JUnit sẽ tìm các fatory method cùng tên với test method.

@DisplayName("Should calculate the correct sum")
    @ParameterizedTest(name = "{index} => a={0}, b={1}, sum={2}")
    @MethodSource() // hmm, khong co ten duoc cung cap
    void testParameterizedTest(int a, int b, int sum, String message) {
        assertEquals(sum, a + b);
        assertTrue(message != null && message != "");
    }

JUnit sẽ cố tìm kiếm Factory method có tên testParameterizedTest(), nếu không tìm thấy test method sẽ bị fail.

Chúng ta có thể sử dụng các Factory method nằm ở các class khác bằng cú pháp

package.class#factorymethod

Ví dụ mình đặt sumProvider() trong class ParameterizedTestExample đặt trong package parameterizedTest.

@DisplayName("Should calculate the correct sum")
@ParameterizedTest(name = "{index} => a={0}, b={1}, sum={2}")
@MethodSource("parameterizedTest.ParameterizedTestExample#sumProvider")
void testParameterizedTest(int a, int b, int sum, String message) {
    assertEquals(sum, a + b);
    assertTrue(message != null && message != "");
}

Tóm lược

@Parameterized hoàn toàn có thể thay thế @Test nên các bạn không cần thêm @Test khi đã có @Parameterized. @Parameterized Đặc biệt biệt hữu để tiết nhiều tham số đầu vào cho một test case, thay vì phải tại nhiều test method mà mỗi method là cái giá trị tham số đầu vào khác nhau cho 1 test case.

Nếu các bạn gặp khó khăn trong quá trình thực nghiệm, có thể checkout source mình đã soạn sẵn tại github repository.

Nguồn tham khảo

https://www.baeldung.com/parameterized-tests-junit-5

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

https://www.petrikainulainen.net/programming/testing/junit-5-tutorial-writing-parameterized-tests/t

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x