JUnit 5 – Rozszerzenia i wstrzykiwanie zależności, część 1
Mechanizm runnerów znany z JUnit 4 był przydatny, ale ograniczony jak prędkość w strefie zamieszkania. Główną wadą był brak możliwości użycia wielu runnerów naraz. Musieliśmy się zdecydować, czy chcemy użyć runnera do Springa, Mockito czy może do parametrów. To był poważny problem. Decydując się na jeden z runnerów, zgadzaliśmy się, na ręczną konfigurację reszty elementów albo rezygnację z niektórych featurów.
TestNG podszedł do problemu trochę inaczej. Udostępniał API, które wykorzystywało Google Guice i pozwalało na konfigurowanie zależności jako modułów. W połączeniu z innymi standardowymi elementami API mogliśmy uzyskać całkiem ładny efekt. To, czego nie mogliśmy zrobić za pomocą TestNG, to uzyskanie dostępu do kontekstu na poziomie testu oraz do kontekstu na poziomie „jakieś magicznej klasy”, która będzie uruchamiania przed testem.
JUnit 5 udostępnia te mechanizmy w różny sposób. Wszystko zależy, co chcemy osiągnąć.
Wstrzykiwanie zależności
Wiemy już jak posługiwać się parametrami w testach. Jest to jedna z form wstrzykiwania zależności, ale nie jedyna. W dodatku ograniczona tylko do metod testowych. Co, jeżeli chcemy wstrzyknąć coś do konstruktora, albo do metody @BeforeX/@AfterX? Tu z pomocą przychodzi nam ParameterReslover. Jest to interfejs, którego implementacje zostaną wywołane w momencie, gdy silnik będzie chciał wywołać konstruktor, metodę testową, albo metodę oznaczoną adnotację, a wywoływany element będzie przyjmować parametry. Istnieje kilka standardowych implementacji. Na przykład RepetitionInfoParameterResolver, który pozwala na dobranie się do informacji o aktualnym teście powtarzalnym. Kolejną implementacją jest TestInfoParameterResolver, który zawiera informacje o aktualnym teście, a chcąc zwrócić dodatkowe informacje z testu, możemy użyć TestReporterParameterResolver. Każda z tych implementacji ma powiązany ze sobą typ, który obsługuje:
- RepetitionInfoParameterResolver – możemy jako parametr przyjmować RepetitionInfo.
- TestInfoParameterResolver – możemy jako parametr przyjmować TestInfo.
- TestReporterParameterResolver – możemy jako parametr przyjmować TestReporter.
Poniżej przykładowa klasa testowa, która wykorzystuje wszystkie te elementy.
Listing 1. Wykorzystanie standardowych ParametrResolver
public class FizzBuzzJUnit5StandardParameterResolversTest {
private FizzBuzz sut;
@BeforeAll
static void classSetup(TestInfo testInfo) {
Logger.getLogger("JUnit 4").info(
String
.format("Test from %s started at %s",
testInfo.getTestClass().map(Class::getName).get(),
LocalDateTime.now()
)
);
}
@AfterAll
static void classTeardown(TestInfo testInfo) {
Logger.getLogger("JUnit 4").info(
String
.format("Test from %s finished at %s",
testInfo.getTestClass().map(Class::getName).get(),
LocalDateTime.now()
)
);
}
@BeforeEach
public void setup(TestInfo testInfo) {
Logger.getLogger("JUnit 4").info(
String
.format("Test %s from %s started at %s",
testInfo.getDisplayName(),
testInfo.getTestMethod().map(Method::getName).get(),
LocalDateTime.now()
)
);
sut = new FizzBuzz();
}
@AfterEach
public void tearDown(TestInfo testInfo) {
sut = null;
Logger.getLogger("JUnit 4").info(
String
.format("Test %s from %s finished at %s",
testInfo.getDisplayName(),
testInfo.getTestMethod().map(Method::getName).get(),
LocalDateTime.now()
)
);
}
@RepeatedTest(10)
public void shouldReturnFizzBuzzIfDiv3And5(RepetitionInfo repetitionInfo, TestInfo testInfo) throws Exception {
Logger.getLogger("JUnit 4").info(
String
.format("Running %s %s of %s",
testInfo.getTestMethod().map(Method::getName).get(),
repetitionInfo.getCurrentRepetition(),
repetitionInfo.getTotalRepetitions()
)
);
assertEquals("FizzBuzz", sut.fizzBuzz(15));
}
@Test
public void shouldReturnBuzzIfDiv5(TestReporter testReporter) throws Exception {
testReporter.publishEntry("Do this stuff", "too often");
assertEquals("Buzz", sut.fizzBuzz(5));
}
@Test
public void shouldReturnFizzIfDiv3() throws Exception {
assertEquals("Fizz", sut.fizzBuzz(3));
}
@Test
public void shouldReturnVal() throws Exception {
assertEquals("2", sut.fizzBuzz(2));
}
}
To, co boli, to biedne API TestReporter. Jest to wrapper na mapę, którą można użyć „gdzieś dalej”. Tym miejscem gdzieś dalej jest implementacja TestExecutionListener, którą należy zarejestrować w Launcher. Ten jest elementem platformy i możemy do niego coś dorzucić tylko z własnego silnika. Mamy zatem zamkniętą drogę do wykorzystania tego interfejsu na poziomie pojedynczego testu (albo jeszcze tego nie rozkminiłem, co też jest prawdopodobne). Dla przyzwoitości:
Listing 2. Przykładowy TestExecutionListener
public class CustomTestExecutionListener implements TestExecutionListener {
@Override
public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry entry) {
Logger.getLogger("JUnit 4").info(
String
.format("***** Test %s finished with results: %s *****",
testIdentifier.getDisplayName(),
entry.getKeyValuePairs().toString()
)
);
}
}
Podsumowanie
W pierwszej części omówiliśmy standardowe elementy API, które można wstrzyknąć do testu. Oczywiście możemy zaimplementować własny ParameterResolver, ale jego użycie wymaga stworzenia rozszerzenia. O rozszerzeniach w następnej części.