Usprawnij testy jednostkowe przy użyciu parametryzacji

Aleksandra Bieńkowska

Jak uruchomić ten sam test JUnit z różnymi zestawami argumentów? Najlepiej wykorzystując testy parametryzowane JUnit. W tym artykule pokażę Ci na podstawie przykładów, w jaki sposób można wykorzystać je w praktyce.

Po co nam testy parametryzowane JUnit?

Rozwiązanie to umożliwia uruchamianie testu z argumentami zdefiniowanymi z użyciem różnorodnych źródeł danych. Aby zaimplementować testy parametryzowane, należy skorzystać z adnotacji @ParameterizedTest oraz określić źródło argumentów. Każde wykonanie takiego testu przebiega według tego samego cyklu życia metody, co w przypadku @Test. Metoda @BeforeEach jest np. wykonywana przed każdym wywołaniem testu parametryzowanego. Te wywołania są sekwencyjnie wyświetlane w drzewie testów w środowisku IDE.

Jak zacząć?

Aby zacząć korzystać z testów parametryzowanych, musisz dodać zależności junit-jupiter-params. W tym artykule korzystam z następującej zależności w Maven:

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

Na potrzeby tego tekstu wykorzystam poniższą metodę, która przeprowadza zautomatyzowaną ocenę złożoności zadania na podstawie wielu parametrów. Spójrz:

public TaskLevel assessTaskLevel (int subtasksNumber,
                                  int avgResolutionTimeInMinutes,
                                  int positionInTasksRank,
                                  String taskLabel,
                                  TaskLevel avgUsersOpinion) {
    if (subtasksNumber > 10 || avgTimeInMinutes > 90 
                            || avgUsersOpinion == TaskLevel.HIGH) {
        return TaskLevel.HIGH;
    } else if (subtasksNumber > 5 && positionInTasksRank < 10 
                            && "M".equals(taskLabel)) {
        return TaskLevel.MEDIUM;
    } else {
        return TaskLevel.LOW;
    }
}

Parametryzacja testów z jednym argumentem przy użyciu adnotacji @ValueSource

Na początku przetestuję metodę oceny zadania przy użyciu pojedynczego parametru avgResolutionTimeInMinutes. W tym celu skorzystam z adnotacji @ValueSource, która pozwala zdefiniować pojedynczą tablicę literałów. Umożliwia ona dostarczenie tylko jednego argumentu dla każdego wywołania testu, w tym przypadku jest to avgResolutionTimeInMinutes. Wartości pozostałych argumentów metody są stałe dla każdego wywołania. Test wygląda następująco:

@ParameterizedTest
@ValueSource(ints = {91, 100, 150, 200, Integer.MAX_VALUE})
void shouldTestBeAssessedAsHigh(int avgResolutionTime) {
    int subtasksNumber = 8;
    int positionInRank = 3;
    String taskLabel = "B";
    TaskLevel avgUsersOpinion = TaskLevel.MEDIUM;

    assertEquals(TaskLevel.HIGH, 
        TaskAssessment.assessTaskLeve(subtasksNumber, avgResolutionTime, positionInRank, taskLabel, avgUsersOpinion)
    );
}

Obsługa parametru pustego i null

JUnit dostarcza wygodne narzędzia do testowania z wykorzystaniem wartości null i pustych poprzez adnotacje takie jak @NullSource, @EmptySource, @NullAndEmptySource. Poniżej przedstawiam przykład parametryzacji argumentu taskLabel wartością pustą i null:

@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {"B","C","D"})
void shouldTaskBeAssessedAsHigh(String taskLabel) {
    int subtasksNumber = 8;
    int positionInRank = 3;
    int avgResolutionTime = 100;
    TaskLevel avgUsersOpinion = TaskLevel.MEDIUM;

    assertEquals(TaskLevel.HIGH, 
        TaskAssessment.assessTaskLevel(subtasksNumber, avgResolutionTime, positionInRank, taskLabel, avgUsersOpinion)
    );
}

Wartości wyliczeniowe jako zmienne argumenty testowe

JUnit umożliwia także parametryzację testów wartościami typu wyliczeniowego za pomocą adnotacji @EnumSource. Jeden ze sposobów użycia przedstawiam w poniższym przykładzie:

@ParameterizedTest
@EnumSource(mode = EnumSource.Mode.INCLUDE, names = {"HIGH", "UPPER_LEVEL"})
void shouldTaskBeAssessedAsHighByTaskLevel(TaskLevel avgUsersOpinion) {
    int subtasksNumber = 8;
    int positionInRank = 3;
    int avgResolutionTime = 60;
    String taskLabel = "B";

    assertEquals(TaskLevel.HIGH, 
    TaskAssessment.assessTaskLevel(subtasksNumber, avgResolutionTime, positionInRank, taskLabel, avgUsersOpinion));
}

W tym przypadku korzystam z domyślnego trybu INCLUDE, jednak dostępne są także alternatywne tryby, np. EXCLUDE lub MATCH_ALL.

Parametryzacja testów z wieloma argumentami przy użyciu adnotacji @MethodSource

Do tej pory parametryzowałam testy z pojedynczym argumentem. Co jednak, jeśli chcemy parametryzować testy wieloma argumentami? Przyjrzyj się poniższemu przykładowi:

@ParameterizedTest
@MethodSource("provideTaskAssessmentArguments")
void shouldTaskBeAssessed ( int subtasksNumber,
                            int positionInRank,
                            int avgResolutionTime,
                            String taskLabel,
                            TaskLevel avgUsersOpinion,
                            TaskLevel expectedResult) {
    assertEquals(expectedResult, 
    TaskAssessment.assessTaskLevel(subtasksNumber, avgResolutionTime, positionInRank, taskLabel, avgUsersOpinion));
}

static Stream<Arguments> provideTaskAssessmentArguments() {
    return Stream.of(
            arguments(8, 3, 60, "M", TaskLevel.MEDIUM, TaskLevel.MEDIUM),
            arguments(8, 3, 60, "B", TaskLevel.UPPER_LEVEL, TaskLevel.HIGH),
            arguments(8, 3, 60, "", TaskLevel.UPPER_LEVEL, TaskLevel.HIGH),
            arguments(8, 3, 60, null, TaskLevel.UPPER_LEVEL, TaskLevel.HIGH)
    );
}

W tym przypadku korzystam z metody 'provideTaskAssessmentArguments’ jako źródła różnych zestawów argumentów. Jej nazwa w adnotacji to @MethodSource. Metoda ta zwraca strumień elementów predefiniowanej klasy Arguments. Zauważ, że zamiast strumienia możesz użyć dowolnej struktury danych, która może zostać przekształcona w strumień, np. Collection, Iterable czy strumienie typów prymitywnych, np. DoubleStream, IntStream.

Adnotację @MethodSource można również zastosować do parametryzowania testów z jednym argumentem. W tym przypadku, zamiast korzystać ze strumienia elementów typu Arguments, można używać innych typów, takich jak String. Spójrz:

@ParameterizedTest
@MethodSource("provideTaskLabels")
void shouldTaskBeAssessedByTaskLabel(String taskLabel) {
    int subtasksNumber = 8;
    int positionInRank = 7;
    int avgResolutionTime = 60;
    TaskLevel avgUsersOpinion = TaskLevel.HIGH;
    assertEquals(TaskLevel.HIGH, 
    TaskAssessment.assessTaskLevel(subtasksNumber, avgResolutionTime, positionInRank, taskLabel, avgUsersOpinion));
}

static Stream<String> provideTaskLabels() {
    return Stream.of("A", "B", "C");
}

Parametryzacja testów z wieloma argumentami przy użyciu adnotacji @ArgumentsSource

Ten sam strumień argumentów, co opisany powyżej, można dostarczyć przez metodę zaimplementowaną w osobnej klasie. W tym celu korzystam z adnotacji @ArgumentsSource. Zobacz poniższy przykład:

public class TestAssessmentArgumentsProvider implements ArgumentsProvider {

    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return Stream.of(
            arguments(8, 5, 60, "M", TaskLevel.MEDIUM, TaskLevel.MEDIUM),
            arguments(8, 3, 60, "B", TaskLevel.UPPER_LEVEL, TaskLevel.HIGH),
            arguments(8, 3, 60, "", TaskLevel.UPPER_LEVEL, TaskLevel.HIGH),
            arguments(8, 3, 60, null, TaskLevel.UPPER_LEVEL, TaskLevel.HIGH)
        );
    }
}

@ParameterizedTest
@ArgumentsSource(TestAssessmentArgumentsProvider.class)
void shouldTaskBeAssessedWithParameters(int subtasksNumber,
                                        int positionInRank,
                                        int avgResolutionTime,
                                        String taskLabel,
                                        TaskLevel avgUsersOpinion,
                                        TaskLevel expectedResult) {

    assertEquals(expectedResult, 
    TaskAssessment.assessTaskLevel(subtasksNumber, avgResolutionTime, positionInRank, taskLabel, avgUsersOpinion));
}

Implementację ArgumentsProvider należy zadeklarować albo jako klasę najwyższego poziomu, albo jako statyczną klasę zagnieżdżoną.

Parametryzacja testu przy użyciu adnotacji @CsvSource

Kolejną opcją parametryzacji testu wieloma argumentami jest skorzystanie z adnotacji @CsvSource. Ta adnotacja umożliwia reprezentację list argumentów jako wartości oddzielonych separatorem, którym domyślnie jest przecinek. Każdy ciąg znaków podany w atrybucie value adnotacji @CsvSource reprezentuje rekord w formacie CSV i odpowiada jednemu wywołaniu testu. W poniższym przykładzie ten sam zestaw argumentów co wcześniej jest dostarczany za pomocą adnotacji @CsvSource:

@ParameterizedTest
@CsvSource({
        "8, 5, 60, 'M', 'MEDIUM', 'MEDIUM'",
        "8, 3, 60, 'B', 'UPPER_LEVEL', 'HIGH'",
        "8, 3, 60, '', 'UPPER_LEVEL', 'HIGH'",
        "8, 3, 60,, 'UPPER_LEVEL', 'HIGH'"
})
void shouldTaskBeAssessedWithParameters(int subtasksNumber,
                                        int positionInRank,
                                        int avgResolutionTime,
                                        String taskLabel,
                                        TaskLevel avgUsersOpinion,
                                        TaskLevel expectedResult) {

    assertEquals(expectedResult, 
    TaskAssessment.assessTaskLevel(subtasksNumber, avgResolutionTime, positionInRank, taskLabel, avgUsersOpinion));
}

Zauważm, że wartości ciągów znaków są zawarte w pojedynczych cudzysłowach (pusty ciąg znaków jest reprezentowany jako ”), a brak wartości między separatorami oznacza wartość null.

Dostarczanie parametrów testowych z pliku przy użyciu adnotacji @CsvFileSource

JUnit umożliwia zdefiniowanie list argumentów w formacie csv w plikach znajdujących się w classpath lub w lokalnym systemie plików. Aby to osiągnąć, możesz skorzystać z adnotacji @CsvFileSource, jak pokazuję w poniższym przykładzie:

@ParameterizedTest
@CsvFileSource(files = "src/test/resources/arguments.csv")
void shouldTaskBeAssessedWithParameters(int subtasksNumber,
                                        int positionInRank,
                                        int avgResolutionTime,
                                        String taskLabel,
                                        TaskLevel avgUsersOpinion,
                                        TaskLevel expectedResult) {

    assertEquals(expectedResult, 
    TaskAssessment.assessTaskLevel(subtasksNumber, avgResolutionTime, positionInRank, taskLabel, avgUsersOpinion));
}

Wartości argumentów umieściłam w osobnym pliku .csv:

8, 5, 60, "M", "MEDIUM", "MEDIUM",
8, 3, 60, "B", "UPPER_LEVEL", "HIGH",
8, 3, 60, "", "UPPER_LEVEL", "HIGH",
8, 3, 60,, "UPPER_LEVEL", "HIGH"

Zauważ, że tym razem używam podwójnych cudzysłowów do oznaczania sekwencji znaków. Wartość pustego ciągu nieobjęta cudzysłowem jest zamieniana na null.

Podsumowanie

W tym artykule opisałam sposoby parametryzowania testów przy użyciu JUnit 5. Dzięki obszernemu zestawowi narzędzi, metod i adnotacji umożliwia on wygodną parametryzację testów z wykorzystaniem różnych rodzajów źródeł danych.

Odnośniki

Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

Skontaktuj się z nami