Usprawnij testy jednostkowe przy użyciu parametryzacji
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.