Patatajam na Confiturę po raz wtóry

W zeszłym roku zaufaliście mi i mogłem wystąpić na scenie Confitury. Skończyło się to jak się skończyło 🙂 W tym roku spotkamy się raz jeszcze. Tym razem będę mówić o JUnit 5 🙂

Dziękuję za zaufanie i wiarę, że dam radę.

Numlock – bo kto bogatemu zabroni

Wczorajszy dzień zakończył się lekką beką wśród znajomych, bo Apple wypuściło klawiaturę z polem numeryczny.

Brawo Apple. Szkoda, że Applefobia już od dawna nie działa. No ale… Papple to papple, a ja od pewnego czasu w pracy testuję „klawiaturę z biedronki”, czyli klawisze marki Hykker.

Żeby nie było, to w domu używam Razer BlackWidow Ultimate Stealth 2014

i ten wpis będzie poniekąd porównaniem tych dwóch klawiatur. Przy czym Hykkera używam raptem ze trzy tygodnie, a Razerem popełniłem już dość dużo postów i kodu. Dlatego nie będzie to pełne porównanie. Jest jednak okazja zatem czemu by nie.

Jak to wygląda – czyli wieś tańczy, wieś śpiewa

Produkty gamingowe odznaczają się swoistą, krzykliwą estetyką. Im więcej diodek, podświetlanych i mrugających elementów tym lepiej. Do tego kształt, który przypomina dziecko duetu AA – Aliena z Aventadorem. Tu obło, tam ostro, a całość nie ma prawa wejść do standardowej przystawki na komputer w typowym biurku.

Klawiatura Hykker nie odbiega od tego wzorca. Podświetlenie aż sześcioma kolorami, prawie jak produkty linii Razer Chroma. Prawie… zamiast płynnych przejść kolorów, każdy rząd klawiszy dostał swój kolorek. Cóż… Kolory, kolorami, ale klawisze nie znajdują się w obudowie. Znaczy się, klawiatura ma obudowę, chroni ona elektronikę, ale same klawisze wystają ponad. Podobnie jak przełączniki. O dziwo łatwiej jest to wyczyścić niż Razera, bo są „dyfuzory”.

Inną ciekawą cechą jest sposób, w jaki skręcono obudowę. Dziesięć śrub dostępnych od góry, z czego sześć to philipsy (krzyżakowe), a cztery to alieny (inbusowe). Zatem nie można klawiatury rozkręcić jednym śrubokrętem.

  • Ja się, kurwa zastanawiam Władziu, jakim debilem był inżynier, który to wymyślił.
  • Heniu, on inżynierem to był chujowym, ale zobacz, że sprzeda ci wkrętak i klucz, a nie tylko wkrętak.
  • Władziu… ty masz racje, mistrz marketingu, się kurwa znalazł.

Kolejna sprawa to taki nalot na klawiaturze jak ja wyjąłem z pudełka. Nie był to kurz, bo dopiero po przetarciu na mokro zniknął.

Ostatnim elementem wartym uwagi jest kabel. Solidnie zamontowany w obudowie, cały w czarnym oplocie ze sztucznych włókien. Podsumowując, na pierwszy rzut oka klawiatura wygląda na solidną.

Jak to działa

Pierwsza rzecz, jaka zwróciła moją uwagę, po podpięciu do komputera, to bardzo długi boot-time. Dość dobrze pamiętam czasy, debiutu technologii P&P (plug and play) i wielkie wow, że można sprzęt podpiąć do działającego komputera i sam się konfiguruje. Hykker niestety potrzebował dość dużo czasu, by wystartować. Zarówno pierwsze podłączenie, hot swap ze starą klawiaturą, jak i po kilku restartach kompa wyglądało tak, że klawiatura zapalała jedną białą diodę na środku i „wisiała” bez reakcji przez kilkanaście sekund. Razerowa klawiatura działa podobnie, ale czas startu jest znacznie krótszy (1-2 sekundy).

Klawiatura ma budżetową wersję przełączników Cherry MX Blue. Nie jest to podróbka. Raczej tani zamiennik. Przełączniki są dość głośnie, ale nie tak jak klawisze w maszynie do pisania. Razer ma swoje przełączniku orange, które pracują ciszej i płynniej. Tu czuć klawiaturę i trzeba pokonywać opór przycisków. Mnie osobiście to pasuje, ponieważ mam ci jak pierdolniecie w palcu. Cóż, pisać uczyłem się na maszynie i mi zostało 🙂 Jedyne co przeszkadza, to niewyważona spacja. Czasami nie łapie, bo nacisnę ją przy samej krawędzi.

Nie ma tu też numpada. Jest to dla mnie nowość, bo całe życie pracowałem na klawiaturach, które miały panel numeryczny. Pewien czas temu musiałem przestawić się, bo w Razerze padło numeryczne 0 i używanie numpada stało się niewygodne. Tu po prostu go nie ma. W moim odczuciu to nie jest złe. Najbardziej brakuje mi jednak nie klawiszy z cyframi, a Enter. Oznacza to też, że nie mogę w łatwy sposób wpisywać znaków, które używają alt-code. Próby włączenia numlocka za pomocą klawisza Fn… nie po prostu nie. Zresztą Windows i tak ma słaby układ klawiatury, bo brakuje mi np. znaków „ i ”.

Klawiatura ma też stosunkowo wysoki czas reakcji. Testowałem na kilku gierkach i niestety działała wolniej niż zwykła biurowa Della, że o Razerze nie wspomnę.

Podsumowanie

Jak masz wolne 150PLN i chcesz mieć klawiaturę mechaniczną do pracy, to będzie ok. Jeżeli jednak miałbym wskazać inną klawiaturę w tym budżecie to raczej A4Tech Bloody B318, które co prawda dupy nie urywa, ale ma trochę bajerów w rodzaju numpada, będzie moda, bo Apple, klawiszy multimedialnych czy makro.

Klawiatura Hykker jest ok. Nie jest zła, ale to bardzo przeciętne klawisze, które jak na razie, wydają się trwałe. Post napisałem z użyciem tej klawiatury i jest OK.

JUnit 5 – założenia i twierdzenia

Istotą każdego testu jest sprawdzenie jakiegoś twierdzenia. Testy bez asercji są jak porno bez fabuły. Wszystko się jebie i w sumie nie wiadomo dlaczego. Dziś przyjrzymy się jakie możliwości w tym zakresie oferuje nam JUnit 5.

Założenia

Czyli to, co występuje pod nazwą Assumptions. Mechanizm ten znamy już z poprzedniej wersji frameworku i w nowej wersji nie został on zmieniony. Dodano kilka metod tak, by w pełni wykorzystać możliwości Javy 8, ale poza tym nie ma znaczących zmian. Ok, ale czym są założenia? W poprzednim poście omawialiśmy tworzenie własnych rozszerzeń. Pierwszy przykład opierał się o analizę adnotacji na metodach testowych i nieuruchamianie testów, które nie spełniały pewnych warunków.
Założenia mają podobne zadanie. Jednak realizują je w inny sposób:

Listing 1. Przykład użycia Assumeptions

public class FizzBuzzJUnit5AssumeTest {

    private FizzBuzz sut;

    @BeforeEach
    public void setup() {
        sut = new FizzBuzz();
    }

    @Test
    public void shouldReturnFizzBuzzIfDiv3And5() throws Exception {
        Assumptions.assumeTrue(getEnvAssumption("CI"), "Not on CI or DEV");
        assertEquals("FizzBuzz", sut.fizzBuzz(15));
    }

    @Test
    public void shouldReturnBuzzIfDiv5() throws Exception {
        Assumptions.assumeTrue(getEnvAssumption("GUI"), "Not on GUI or DEV");
        assertEquals("Buzz", sut.fizzBuzz(5));
    }

    @Test
    public void shouldReturnFizzIfDiv3() throws Exception {
        Assumptions.assumeTrue(getEnvAssumption("NOGUI"), "Not on NOGUI or DEV");
        assertEquals("Fizz", sut.fizzBuzz(3));
    }

    @Test
    public void shouldReturnVal() throws Exception {
        Assumptions.assumeTrue(getEnvAssumption(""), "Not on NOGUI or DEV");
        assertEquals("2", sut.fizzBuzz(2));
    }

    private BooleanSupplier getEnvAssumption(String isIt) {
        return () -> {
            String envName = Optional.ofNullable(System.getenv("ci_name")).orElse("DEV");
            return Optional.of(envName).map(s -> s.equals(isIt) || s.equals("DEV")).get();
        };
    }
}

Jeżeli teraz uruchomimy nasze testy w środowisku, gdzie zmienna ci_name ma wartość inną niż CI, GUI, NOGUI, DEV albo nie jest zdefiniowana, to otrzymamy komunikat:

Listing 2. Log uruchomienia testu:

org.opentest4j.TestAbortedException: Assumption failed: Not on CI or DEV


	at org.junit.jupiter.api.Assumptions.throwTestAbortedException(Assumptions.java:246)
	at org.junit.jupiter.api.Assumptions.assumeTrue(Assumptions.java:119)
	at org.junit.jupiter.api.Assumptions.assumeTrue(Assumptions.java:80)
	at pl.koziolekweb.blog.fizzbuzz.assertions.FizzBuzzJUnit5AssumeTest.shouldReturnFizzBuzzIfDiv3And5(FizzBuzzJUnit5AssumeTest.java:27)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:316)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:114)
	at org.junit.jupiter.engine.descriptor.MethodTestDescriptor.lambda$invokeTestMethod$6(MethodTestDescriptor.java:171)
	at org.junit.jupiter.engine.execution.ThrowableCollector.execute(ThrowableCollector.java:40)
	at org.junit.jupiter.engine.descriptor.MethodTestDescriptor.invokeTestMethod(MethodTestDescriptor.java:168)
	at org.junit.jupiter.engine.descriptor.MethodTestDescriptor.execute(MethodTestDescriptor.java:115)
	at org.junit.jupiter.engine.descriptor.MethodTestDescriptor.execute(MethodTestDescriptor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.lambda$execute$1(HierarchicalTestExecutor.java:81)
	at org.junit.platform.engine.support.hierarchical.SingleTestExecutor.executeSafely(SingleTestExecutor.java:66)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:76)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.lambda$execute$1(HierarchicalTestExecutor.java:91)
	at org.junit.platform.engine.support.hierarchical.SingleTestExecutor.executeSafely(SingleTestExecutor.java:66)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:76)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.lambda$execute$1(HierarchicalTestExecutor.java:91)
	at org.junit.platform.engine.support.hierarchical.SingleTestExecutor.executeSafely(SingleTestExecutor.java:66)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:76)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:51)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:43)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:137)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:87)
	at org.junit.platform.launcher.Launcher.execute(Launcher.java:93)
	at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:61)
	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

Test został przerwany, ale wyjątek TestAbortedException będzie obsłużony inaczej niż AssertionFailedError (btw, kolejna fajna zmiana nazwy). Test powinien zostać oznaczony jako niewykonany. Podobnie do testu oznaczonego @Disabled. Na czym polega różnica?

@Disabled oraz tagi

Adnotacji tej powinniśmy użyć, jeżeli chcemy wyłączyć test lub wszystkie testy w klasie. Podobnie z tagami. Używamy ich do przeprowadzania masowej akcji bez wnikania w szczegóły.

Rozszerzenie

Rozszerzenia są bardzo elastyczne i powinniśmy ich używać wszędzie tam, gdzie mamy do czynienia ze złożoną logiką. Jednocześnie należy pamiętać, że rozszerzenia są mechanizmem pozwalającym dodać coś do środowiska testowego, ale nie koniecznie. W dodatku ze względu na statyczną naturę adnotacji nie możemy z ich pomocą dynamicznie włączać i wyłączać testów. Rozszerzenia nie wiedzą nic o teście, a test nie wie nic o rozszerzeniu.

Założenia

Mechanizm założeń jest silnie powiązany z testami. Założenia definiujemy na tym samym poziomie co test. Możemy wykorzystać tu wiedzę o teście, by podjąć odpowiednią decyzję. Jest to bardzo elastyczne rozwiązanie, ale ortogonalne do rozszerzeń.

Wiemy już, czym są założenia, przejdźmy do twierdzeń.

Twierdzenia, czyli asercje

Pod tym względem JUnit 5 udostępnił na kilka fajnych rozwiązań.

Testy z wieloma asercjami

Czasami jest tak, że nasz test powinien być wykonany dla różnych danych wejściowych. Możemy do tego wykorzystać parametry. Jednak zdarza się sytuacja odwrotna. Test opiera się na wielu asercjach. W klasycznym podejściu test wysypie się, gdy pierwsza z asercji nie będzie spełniona. Utrudnia to pracę. Piszemy, odpalamy, poprawiamy jeden błąd tylko po to, by kolejne uruchomienie zwróciło nam kolejnego babola. Szczególnie bolesne jest, to gdy asercje nie są ze sobą silnie powiązane, ich kolejność nie gra roli. By rozwiązać ten problem, możemy użyć assertAll:

Listing 3. Użycie assertAll

public class FizzBuzzJUnit5AssertAllTest {

	private FizzBuzz sut;

	@BeforeEach
	public void setup() {
		sut = new FizzBuzz();
	}

	@Test
	public void shouldReturnFizzBuzzIfDiv3And5() throws Exception {
		assertAll(
				() -> assertEquals("FizzBuzz", sut.fizzBuzz(16)),
				() -> assertEquals("FizzBuzz", sut.fizzBuzz(30)),
				() -> assertEquals("FizzBuzz", sut.fizzBuzz(151))
		);
	}
}

Po uruchomieniu tego testu zobaczymy dwa błędy. Od razu otrzymamy pełen obraz problemu. Jest to przydatna informacja jeżeli w naszym teście poza asercjami, dokonujemy weryfikacji mocków. Wtedy mamy elegancko rozdzielone informacje. Kod zwraca poprawny wynik, ale nie wywołuje jakiejś operacji pod spodem.

Testowanie wyjątków

Testowanie czy kod zwrócił wyjątek jest wybitnie upierdliwe. Co prawda mogliśmy użyć @Test(expected), ale to było złe na wielu poziomach. Potem pojawiła się regułka (@Rule) ExpectedException, Ewentualnie można było sobie wyrzeźbić odpowiednią asercję albo użyć AssertJ, ale dopiero to ostatnie rozwiązanie w połączeniu z Javą 8 było sensowne. JUnit 5 wprowadza podobne rozwiązanie:

Listing 4. Użycie assertThrows

public class FizzBuzzJUnit5AssertExceptionTest {

	private FizzBuzz sut;

	@Test
	public void shouldReturnFizzBuzzIfDiv3And5() throws Exception {
		assertThrows(NullPointerException.class, () -> assertEquals("FizzBuzz", sut.fizzBuzz(15)));
	}

	@Test
	public void shouldReturnBuzzIfDiv5() throws Exception {
		assertThrows(NullPointerException.class, () -> assertEquals("Buzz", sut.fizzBuzz(5)));
	}

	@Test
	public void shouldReturnFizzIfDiv3() throws Exception {
		assertThrows(NullPointerException.class, () -> assertEquals("Fizz", sut.fizzBuzz(3)));
	}

	@Test
	public void shouldReturnVal() throws Exception {
		assertThrows(NullPointerException.class, () -> assertEquals("2", sut.fizzBuzz(2)));
	}
}

Elegancko. Możemy zdecydować, gdzie dokładnie spodziewamy się błędu. Jeżeli błąd pojawi się gdzieś poza „sekcją krytyczną” (nie mylić z sekcją krytyczną związaną z synchronizacją), to test się wywali.

Timeout testu

Trochę bardziej złożonym problemem niż weryfikacja wyjątków jest weryfikacja maksymalnego czasu wykonania testu. Podobnie jak w poprzednim wypadku mogliśmy ustawić @Test(timeout) i mieć to z głowy. Jednak znowuż, mamy do czynienia z problemem oderwania tego, co chcemy przetestować, od tego, co w rzeczywistości możemy przetestować. Tak ustawione ograniczenie dotyczy całego testu. Jeżeli przygotowanie danych, w ramach „sekcji given”, a nie @BeforeEach, będzie długie, to test się wysypie, ale nie na tym, co potrzeba! Kolejnym problemem jest brak informacji, o ile przekroczyliśmy wyznaczoną granicę. Zobaczmy zatem, jak to będzie wyglądać:

Listing 5. Użycie assertTimeout i assertTimeoutPreemptively

public class FizzBuzzJUnit5AssertTimeoutTest {

	private FizzBuzz sut;

	@BeforeEach
	public void setup() {
		sut = new FizzBuzz();
	}

	@Test
	public void shouldReturnFizzBuzzIfDiv3And5() throws Exception {
		assertTimeout(Duration.ofMillis(100), () -> {
			assertEquals("FizzBuzz", sut.fizzBuzz(15));
			Thread.sleep(200);
		});
	}

	@Test
	public void shouldReturnBuzzIfDiv5() throws Exception {
		assertTimeout(Duration.ofMillis(100), () -> {
			assertEquals("Buzz", sut.fizzBuzz(5));
			Thread.sleep(200);
		}, "Ups... out of time!");
	}

	@Test
	public void shouldReturnFizzIfDiv3() throws Exception {
		assertTimeoutPreemptively(Duration.ofMillis(100), () -> {
			assertEquals("Fizz", sut.fizzBuzz(3));
			Thread.sleep(200);
		});
	}

	@Test
	public void shouldReturnVal() throws Exception {
		assertTimeoutPreemptively(Duration.ofMillis(100), () -> {
			assertEquals("2", sut.fizzBuzz(2));
			Thread.sleep(200);
		}, "Ups... out of time!");
	}
}

Różnica pomiędzy tymi dwoma metodami będzie widoczna po uruchomieniu. Metoda assertTimeout poczeka na zakończenie testu i w komunikacie poda, o ile przekroczono czas. Metoda assertTimeoutPreemptively, jak sama nazwa wskazuje, prewencyjnie przerwie test i zwróci informację o przekroczeniu czasu oczekiwania.

Podsumowanie

JUnit 5 wprowadza do swojego API asercji kilka rozwiązań znanych z innych bibliotek. Co ważne twórcy wycofali wsparcie dla Matcher z Hamcrest. Nie ma metody assertThat, która akceptowała Matcher. Jednocześnie twórcy rekomendują używanie bibliotek z asercjami, jeżeli te dostarczone w standardowym API będą niewystarczające. Jak dla mnie jest, to duży plus. JUnit nie preferuje użycia konkretnego rozwiązania i pozostawia nam pełną swobodę wyboru.

JUnit 5 – Rozszerzenia i wstrzykiwanie zależności, część 2

W poprzednim wpisie zajmowaliśmy się standardowymi implementacjami ParameterResolver. Na zakończenie wspomniałem, że własna implementacja wymaga konfiguracji na poziomie silnika testów. Takie podejście jest uciążliwe i wiąże się m.in. z implementacją własnego silnika lub hackowaniem istniejącego. To jest trochę krzywe. Twórcy biblioteki JUnit 5 mając świadomość, że takie rozwiązanie jest kiepskie, przygotowali mechanizm rozszerzeń.

Rozszerzenia można z grubsza podzielić na dwie grupy. Pierwsza to rozszerzenia deklaratywne. Tym przyjrzymy się dzisiaj. Druga to rozszerzenia oparte o mechanizm SPI. W tym przypadku wykorzystujemy rejestr serwisów, a rozszerzenia są rejestrowane w momencie startu kontenera. Przykładem tego typu rozszerzenia jest junit-jupiter-params, z którym mieliśmy już do czynienia. Tym mechanizmem zajmiemy się później (znacznie później, jak go rozkminię 😉 ).

Problem

Zanim przejdę do opisu mechanizmu rozszerzeń, to pozwolę sobie na zdefiniowanie problemu, który będzie stanowić tło naszych rozważań. Mamy pewną ilość testów integracyjnych, z których część może być uruchomiona tylko i wyłącznie na określonych maszynach w ramach CI. Maszyna identyfikuje się za pomocą zmiennej systemowej. Ponadto, chcemy by testy te, można było uruchomić na maszynie deweloperskiej oznaczonej jako DEV. Programiści nie muszą nic konfigurować na swoich maszynach.

Inaczej mówiąc, jeżeli jest obecna zmienna środowiskowa ci_name, to test będzie uruchomiony, jeżeli jej wartość odpowiada wartości z adnotacji. Jeżeli nie jest definiowana albo jest równa DEV, to test też jest uruchamiany.

Rozwiązanie

Zacznę trochę od dupy strony, czyli od przygotowania mechanizmu, który będzie odpowiadać za oznaczanie testów do uruchomienia. W tym celu należy zaimplementować interfejs TestExecutionCondition. Ma on jedną metodę, evaluate, która przyjmuje TestExtensionContext i zwraca ConditionEvaluationResult. Kod wygląda następująco:

Listing 1. Implementacja TestExecutionCondition

public class SingleTestIntegrationFilter implements TestExecutionCondition {

    private static final String CI_NAME = Optional.ofNullable(
            System.getenv("ci_name")
    )
            .orElse("DEV");

    @Override
    public ConditionEvaluationResult evaluate(TestExtensionContext context) {
        return context.getTestMethod()
                .filter(m -> isAnnotated(m, Integration.class))
                .map(m -> m.getAnnotation(Integration.class).value())
                .filter(((Predicate<String>) s -> CI_NAME.equals(s)).or(s1 -> CI_NAME.equals("DEV")))
                .map($ -> ConditionEvaluationResult.enabled(""))
                .orElse(ConditionEvaluationResult.disabled(format("This test %s cannot be run on %s.", context.getTestMethod().map(Method::getName).get(), CI_NAME)));
    }

}

Przeszukujemy metody oznaczone adnotacją @Integration:

Listing 2. Adnotacja @Integration

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Integration {

    String value() default "DEV";
}

Jeżeli wartość w adnotacji jest inna niż zadeklarowana w zmiennej ci_name, to zwracamy ConditionEvaluationResult.disabled z odpowiednim komunikatem.

Interfejs TestExecutionCondition jest jednym z kilku, które możemy wykorzystać. Zanim jednak przejdę do omówienia innych interfejsów, chciałbym pokazać, jak uruchamiamy nasze rozszerzenie.

Uruchomienie rozszerzenia

Jak wspomniałem na samym początku, w tym wpisie zajmiemy się rozszerzeniami deklaratywnymi. Deklaratywność oznacza tu, że musimy w kodzie explicite wskazać których rozszerzeń używamy. Można pomyśleć o tym mechanizmie jak o runnerach. Różnica polega na tym, że w JUnit 5 możemy użyć wielu rozszerzeń dla jednej klasy testowej.

By uruchomić test z rozszerzeniem należy użyć adnotacji @ExtendWith.

Listing 3. Przykładowy test z rozszerzeniem

@ExtendWith(SingleTestIntegrationFilter.class)
public class FizzBuzzJUnit5CiEnvFilteredIntegrationTest {

	private FizzBuzz sut;

	@BeforeEach
	public void setup() {
		sut = new FizzBuzz();
	}

	@Test
	@Integration("CI")
	public void shouldReturnFizzBuzzIfDiv3And5() throws Exception {
		assertEquals("FizzBuzz", sut.fizzBuzz(15));
	}

	@Test
	@Integration("GUI")
	public void shouldReturnBuzzIfDiv5() throws Exception {
		assertEquals("Buzz", sut.fizzBuzz(5));
	}

	@Test
	@Integration("NOGUI")
	public void shouldReturnFizzIfDiv3() throws Exception {
		assertEquals("Fizz", sut.fizzBuzz(3));
	}

	@Test
	@Integration
	public void shouldReturnVal() throws Exception {
		assertEquals("2", sut.fizzBuzz(2));
	}
}

Mamy tu cztery testy integracyjne, z których każdy może zostać uruchomiony tylko na konkretnych środowiskach CI. Przy czym ostatni z nich, shouldReturnVal, może zostać uruchomiony jedynie na środowisku deweloperskim.

Jak można zauważyć ten mechanizm, nie jest jakoś skomplikowany. Jego zaletą jest prostota implementacji oraz jasność intencji. Dla tego konkretnego przypadku konfiguracja tagów mogłaby być wystarczająca, ale wymagałaby wdrożenia profili. Profile są specyficzne dla mavena i trudno nimi zarządzać z poziomu poma. Złożoność będzie szybko rosła wraz ze wzrostem liczby środowisk. W dodatku użycie filtrów opartych o tagi albo nazwy metod uniemożliwia raportowanie przyczyny wyłączenia testu.

Ogólniejsza forma tego mechanizmu jest reprezentowana przez ContainerExecutionCondition. Wykorzystując ten interfejs, możemy decydować czy należy uruchomić wszystkie testy z kontenera (klasy testowej). Oczywiście implementacja logiki będzie podobna, a różnice istnieją jedynie na poziomie nazw wykorzystywanych klas.

Inne punkty rozszerzeń

Oczywiście rozszerzenia to nie tylko decydowanie o uruchomieniu bądź nie testu. Ciekawym przypadkiem może być wprowadzenie własnej implementacji ParameterResolver.

Własny ParameterResolver

Załóżmy, że nasz test potrzebuje pewnych informacji ze świata. Możemy podać je na kilka sposobów. Najprościej jest stworzyć odpowiedniego mocka i przekazać go jakoś do testu. Jakoś w naszym przypadku oznacza parametr metody testowej. Oczywiście jest to przypadek uproszczony, ponieważ mock może być też polem klasy testowej. W takim wypadku potrzebujemy zaimplementować jeszcze inny interfejs. O tym jednak za chwilę. Najpierw zaimplementujmy nasz własny ParameterResolver:

Listing 4. Własna implementacja ParameterResolver

public class MockParameterResolver implements ParameterResolver {
    @Override
    public boolean supports(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
        return parameterContext.getParameter().isAnnotationPresent(Mock.class);
    }

    @Override
    public Object resolve(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
        return Mockito.mock(parameterContext.getParameter().getType());
    }
}

Cep wydaje się skomplikowanym narzędziem, jeżeli porównamy go z tym kodem. Oczywiście w praktyce należałoby ten kod wzbogacić o kilka elementów np. cache. Tak też robi Mockito Extension, dostępne w repozytorium przykładów.

Konfiguracja instancji klasy testowej

Wspomniałem w poprzednim podpunkcie, że mock może być też polem klasy testowej. By wstrzyknąć takie pole musimy obsłużyć utworzoną, ale jeszcze nie uruchomioną instancję klasy testowej. W tym celu możemy wykorzystać TestInstancePostProcessor:

Listing 5. Implementacja TestInstancePostProcessor

public class MockFieldInjector implements TestInstancePostProcessor {

	@Override
	public void postProcessTestInstance(Object o, ExtensionContext extensionContext) throws Exception {
		MockitoAnnotations.initMocks(o);
	}
}
Wywołania wokół testów

Kolejną grupę rozszerzeń stanowią te, które będą uruchamiane wokół testów. Działają jak Before/After, ale pozwalają na operowanie na instancji klasy testowej albo testu. Oczywiście mamy tu zachowane nazewnictwo i tak mamy do dyspozycji następujące rozszerzenia (podstaw Before/After za XXX):

  • XXXAllCallback – rozszerzenie, to zostanie wywołane przed/po wszystkich testach w ramach danego kontenera (klasy testowej).
  • XXXEachCallback – rozszerzenie, to zostanie wywołane przed/po każdym teście. Przy czym jako test rozumiemy tu metodę testową oraz zdefiniowane w danej klasie testowej metody oznaczone jako @BeforeEach/@AfterEach, uruchamiane wokół metody testowej.
  • XXXTestExecutionCallback – podobnie jak poprzednie rozszerzenie, to też zostanie wywołane przed/po każdym teście, ale w tym przypadku test oznacza tylko metodę testową.

Generalna zasada, której musimy przestrzegać, jest taka, że implementacja rozszerzenia musi posiadać konstruktor bezargumentowy.

Uruchamianie wielu rozszerzeń w jednej klasie

Analizując przypadek z wykorzystaniem mocków, pojawia się pytanie, czy możemy rozszerzyć pojedynczą klasę testową na wiele sposobów? To znaczy, czy w pojedynczej klasie możemy użyć wielu rozszerzeń. Oczywiście tak. Jest to znaczny postęp w porównaniu do runnerów z JUnit4, które mogły występować tylko pojedynczo. Wystarczy wielokrotnie użyć adnotacji @ExtendedWith, z odpowiednimi parametrami:

Listing 6. Test z wieloma rozszerzeniami

@ExtendWith(MockParameterResolver.class)
@ExtendWith(MockFieldInjector.class)
public class FizzBuzzJUnit5MockDiTest {

	@Mock
	private FizzBuzz sut;

	@Test
	public void shouldReturnFizzBuzzIfDiv3And5() throws Exception {
		when(sut.fizzBuzz(anyInt())).then(invocation -> "FizzBuzz");
		assertEquals("FizzBuzz", sut.fizzBuzz(15));
	}

	@Test
	public void shouldReturnBuzzIfDiv5(@Mock FizzBuzz sut) throws Exception {
		when(sut.fizzBuzz(anyInt())).then(invocation -> "Buzz");
		assertEquals("Buzz", sut.fizzBuzz(5));
	}

	@Test
	public void shouldReturnFizzIfDiv3(@Mock FizzBuzz sut) throws Exception {
		when(sut.fizzBuzz(anyInt())).then(invocation -> "Fizz");
		assertEquals("Fizz", sut.fizzBuzz(3));
	}

	@Test
	public void shouldReturnVal(@Mock FizzBuzz sut) throws Exception {
		when(sut.fizzBuzz(anyInt())).then(invocation -> invocation.getArgument(0).toString());
		assertEquals("2", sut.fizzBuzz(2));
	}
}

Podsumowanie

Mechanizm rozszerzeń dostępny w JUnit 5 jest znacznie bardziej elastyczny niż runnery z JUnit 4. Oferuje nam znacznie więcej możliwości i co najważniejsze wykorzystanie go, nie jest skomplikowane. Z drugiej strony, i jest to wada całego JUnita 5, nie wykorzystamy tego mechanizmu w Javie 7 i wcześniejszych.

A na plażę którędy? DevCrowd 2017

Za nami dziewiąta już edycja DevCrowd (Java4People). Dla mnie była to edycja numer siedem 🙂 Pojechałem, posłuchałem, pogadałem i oto wnioski.

Stylowy frontend w praktyce, czyli zderzenie z Angularem i SCSSem – Katarzyna Koszur

Pierwszy raz na scenie i całkiem fajnie wyszło. Brakowało mi trochę efektu łał, ale całkiem fajnie. Na pierwszy ogień poszedł Angular i od razu zostały wytłumaczone zasady nazewnictwa. Całkiem zgrabne wprowadzenie, ale takie trochę zbyt jednostajne. Następnie krótko o SCSS i generatorach. Tu zabrakło mi jednej rzeczy – jak są rozwiązywane mapy css i jak z tego korzystać.

Are you familiar with Test Doubles Patterns? – Sebastian Malaca

Prezentacja Sebastiana była poświęcona tzw. dublerom w testach. Przeszliśmy sobie spacerkiem od prostych „ślepych implementacji”, poprzez null object, aż do mocków i spay-ów. Całkiem fajne przypomnienie, że nie samym Mockito człowiek żyje 🙂

Porty i Adaptery – Dominik Przybysz

Elementy łączące usługi w architekturze heksagonalnej. Chyba najciekawszy fragment tej architektury, a Dominik świetnie go przedstawił. Za kilka tygodni będzie dostępne nagranie z warszawskiego JUGa, gdzie też o tym mówił 🙂 Osobiście uważam tę prezentację za najlepszą na konferencji.

Platformy „zero code” – mity, złudzenia i rzeczywistość – Maciek Próchniak

Czyli co zrobić, by się nie przepracować, a żeby było zrobione. Prezentacja, w której było samo mięsko. Maciek opowiedział nam o tym, jak przerzucili na klienta tworzenie regułek biznesowych. Praktyczne zastosowanie Drools, BPMLa i innych tego typu narzędzi.

Akka-http – functional approach to REST services – Szymon Pacanowski

Wprowadzenie do Akka-HTTP. Szymon przeszedł przez podstawowe funkcjonalności i pokazał, co można z tego wycisnąć. Co prawda całość w Javie, co powodowało, że kod był paskudny, ale z drugiej strony ile można tą Akkę w Scali męczyć.

Podsumowanie

Całkiem miło było 🙂 Choć czułem się paskudnie, bo to był kolejny dzień dłuższej delegacji i wojaży po Polsce. Po cichu trzymam organizatorów za słowo i liczę, że w przyszłym roku będzie nagrywane 🙂

Praktyczne zastosowanie strażników w Elixirze

Trwa przerwa od JUnita. Wybaczcie, ale muszę odpocząć kilka dni od tego tematu. W zamian trochę Elixira i mały praktyczny przykładzik jak można zastosować strażników. Z samym mechanizmem strażników zapoznaliśmy się już wcześniej. Dziś coś, co robiłem przy okazji peselxa, a co finalnie zostało „ostrażnikowane”.

Problem

Z numeru PESEL możemy wyciągnąć informacje o dacie urodzenia. By to zrobić, musimy odpowiednio zinterpretować pierwszą cyfrę miesiąca, zgodnie ze schematem:

  • Jeżeli jest to 0 lub 1, to osoba urodziła się pomiędzy 1900, a 1999 rokiem.
  • Jeżeli jest to 2 lub 3, to osoba urodziła się pomiędzy 2000, a 2099 rokiem.
  • Jeżeli jest to 4 lub 5, to osoba urodziła się pomiędzy 2100, a 2199 rokiem.
  • Jeżeli jest to 6 lub 7, to osoba urodziła się pomiędzy 2200, a 2299 rokiem.
  • Jeżeli jest to 8 lub 9, to osoba urodziła się pomiędzy 1800, a 1899 rokiem.

Piękna ifologia. Piękna. Przykładowo, żeby określić, w którym roku urodziła się dana osoba, możemy napisać:

Listing 1. Wyciąganie roku urodzenia z PESEL

defp calculate_year({[d|u], [f|_], _}) do
 u = to_integer(u)
 cond do
    f == 0 || f == 1 -> 1900 + 10 * d + u
    f == 2 || f == 3 -> 2000 + 10 * d + u
    f == 4 || f == 5 -> 2100 + 10 * d + u
    f == 6 || f == 7 -> 2200 + 10 * d + u
    f == 8 || f == 9 -> 1800 + 10 * d + u
 end
end

Za pomocą cond mogę zdefiniować zestaw if-elseif. Podobnie będzie wyglądać to dla miesiąca, ale tu musimy w razie czego odjąć odpowiednią liczbę, by „znormalizować” numer. Problem z tym kodem polega na tym, że credo, czyli checkstyle dla Elixira, będzie marudziło. Funkcja ma za dużą złożoność. Trudno się z tym nie zgodzić, ponieważ mamy tutaj zaszyte wiele instrukcji warunkowych.

Refaktoryzacja do strażników

Poprawmy kod, przerzucając odpowiedzialność za wybór odpowiedniej ścieżki na maszynę wirtualną. Dzięki czemu nasza funkcja będzie bardzo prosta. W tym celu wykorzystamy strażników, którzy będą opisywać warunki, które musi spełnić cyfra dziesiątek w miesiącu, aby wywołać odpowiednią logikę:

Listing 2. Kod po refaktoryzacji do strażników

defp calculate_year({[d|u], [f|_], _}) when f == 0 or f == 1, do: c_y(1900, d , u)
defp calculate_year({[d|u], [f|_], _}) when f == 2 or f == 3, do: c_y(2000, d , u)
defp calculate_year({[d|u], [f|_], _}) when f == 4 or f == 5, do: c_y(2100, d , u)
defp calculate_year({[d|u], [f|_], _}) when f == 6 or f == 7, do: c_y(2200, d , u)
defp calculate_year({[d|u], [f|_], _}) when f == 8 or f == 9, do: c_y(1800, d , u)

defp c_y(cent, d, u), do: cent + 10 * d + to_integer u

Jak widać, kod stał się znacznie przyjemniejszy. Ponadto, strażnicy pilnują nam pokrycia wszystkich gałęzi. Jeżeli ktoś poda parametr, który nie jest obsługiwany…

Scalar 2017, czyli najmodniejszy buzz word Scali to…

… oczywiście typeclass i monad transformer. W dodatku zrobiony shapelessem albo catsami.

Zamiast opowiadania o poszczególnych prezentacjach, podzielę się z wami tylko tymi, które moim zdaniem trzeba obejrzeć, jak tylko ukażą się nagrania.

Dave Gurnell – Adventures in Meta-Programming

Ciekawe porównanie trzech technik metaprogramowania w Scali. Makra, Shapeless i generowanie kodu. Każda z tych metod ma swoje wady i zalety, a Dave świetnie je porównał. Drugiego dnia trafiłem też na prowadzone przez niego warsztaty Shapeless. Całość jest całkiem zgrabnym wprowadzeniem do problematyki metaprogramowania.

John A. de Goes – Quark: A Purely-Functional Scala DSL for Data Processing & Analytics

Gdy spotkałem Johna przed afterem i zaprowadziłem na miejsce, to ochrona w klubie przyglądała mu się bardzo „dziwnie”. Może to dlatego, że John pod mentalną powłoką świetnego specjalisty od Scali i programowania w ogóle kryje ciało prawdziwego pakiera? Prezentacja poświęcona była najnowszemu, premiera wersji stabilnej miała miejsce na scalarze, narzędziu do analizy danych, opartemu o Scalę, Sparka i trochę magii. Świetna prezentacja.

Gabriele Petronella – Practical Monad Transformers

Kolejna prezentacja, która miała za zadanie wprowadzić w tematykę, tym razem monad transformers. Jest to wysoce wyspecjalizowany typ, pozwalający na kompozycję monad (więcej). Temat istotny, ponieważ coraz częściej używamy różnego rodzaju monad w naszym kodzie i prędzej czy później spotkamy się z problemem ich kompozycji.

Piotr Guzik – Real-time Anomaly Detection Made Easy

Piotrek pracuje dla Allegro i w swojej prezentacji pokazał jak można wyszukiwań anomalii w strumieniu danych. Przy okazji opowiedział trochę o Scali jako takiej i dlaczego R ssie. Prezentacja raczej ciekawostkowa, ale warto 🙂

Maciej Gorywoda – Artificial Neural Networks In Akka

Pamiętam jak wiele lat temu na jakimś obozie aikido, Maciek próbował mi wytłumaczyć, czym są sieci neuronowe i jak to działa. Wtedy jeszcze nie pracowałem jako programista i była, to dla mnie czarna magia. Po latach dostałem ten sam wykład 🙂 W bardzo przystępnej formie. Co prawda więcej tu było biologii niż programowania, ale cały kod jest dostępny na githubie.

Paweł Szulc – Getting More Mileage From Your Monads With MTL

Paweł zaszalał. Opowiedział o monad transformerach z Haskella i zrobił to tak, że musicie zobaczyć. IMO, najlepsza prezentacja całej konferencji.

Valentin Kasas – Carpenters And Cartographers

Bardzo filozoficzna prezentacja, w której Valentin porusza kwestię abstrakcji i metafory w opisie problemu. Jest to o tyle ważne, że jeżeli zrozumiemy różnicę pomiędzy tymi pojęciami, to znacznie łatwiej będzie nam projektować kod. Swoją drogą padła też definicja designu, która może niejednemu architektowi zniszczyć wyobrażenie o świecie.

Podsumowanie

Podobnie jak rok temu spotkaliśmy się w Muzeum Historii Żydów Polskich POLIN, które ze względu na bardzo charakterystyczne wejście można nazwać Muzeum Haskella. Zresztą sami oceńcie:

Museum of Haskell

źródło

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.

JUnit 5 – Fabryki testów

Czasami samo przekazanie parametrów do testów nie wystarcza. Nie są to sytuacje częste, ale mogą się zdarzyć. Jednym z przykładów niech będzie konieczność pobrania danych testowych z jakiejś bazy danych. Nie posiadamy do dyspozycji żadnego narzędzia, w rodzaju @CsvFileSource. Można takie narzędzie napisać. Dodać rozszerzenie do modułu testów parametryzowanych i będzie OK. Ma ono jednak pewne cechy, które parafrazując Twierdzenie GödlaW, można pisać w następujący sposób:

Mechanizm będzie prosty w użyciu np. @DataBaseSource(DbArgumentsProvider.class), ale wymagający wielu dodatkowych elementów do implementacji, albo będzie trudny w użyciu, ale nie będzie wymagać dodatkowych elementów do implementacji np. @DataBaseSource(„DataSourceName”, „QUERY”, ResultSetMapper.class).

I tak źle i tak niedobrze. W dodatku pytanie, co w przypadku innych źródeł danych (JSON, yaml, Excel, XML)? Jak zachowają się testy, gdy zmienimy bazę danych, co w przypadku gdy zapytanie zwraca wyniki w losowej kolejności? Rozwiązaniem są fabryki testów. Mechanizm ten jest obecny w TestNG:

Listing 1. @Factory w TestNG

public class FizzBuzzTestNGFactoryTest {

	@Factory
	public Object[] fizzBuzzTestFactory() {
		return new Object[]{new DividedBy3(),new DividedBy5(), new DividedBy15(), new NotDividedBy3Or5()};
	}

	static class NotDividedBy3Or5 {

		private FizzBuzz sut;

		@BeforeTest
		public void setup() {
			sut = new FizzBuzz();
		}

		@Test
		public void shouldReturnVal() throws Exception {
			assertEquals("2", sut.fizzBuzz(2));
		}
	}

	static class DividedBy5 {

		private FizzBuzz sut;

		@BeforeTest
		public void setup() {
			sut = new FizzBuzz();
		}

		@Test
		public void shouldReturnBuzzIfDiv5() throws Exception {
			assertEquals("Buzz", sut.fizzBuzz(5));
		}
	}

	static class DividedBy3 {
		private FizzBuzz sut;

		@BeforeTest
		public void setup() {
			sut = new FizzBuzz();
		}

		@Test
		public void shouldReturnFizzIfDiv3() throws Exception {
			assertEquals("Fizz", sut.fizzBuzz(3));
		}
	}

	static class DividedBy15 {

		private FizzBuzz sut;

		@BeforeTest
		public void setup() {
			sut = new FizzBuzz();
		}

		@Test
		public void shouldReturnFizzBuzzIfDiv3And5() throws Exception {
			assertEquals("FizzBuzz", sut.fizzBuzz(15));
		}
	}

}

Metoda oznaczona @Factory zwraca tablicę obiektów, które są testami. W tym konkretnym przypadku wykonujemy pracę, którą normalnie robi za nas biblioteka. Jednak można łatwo wyobrazić sobie sytuację, w której proces tworzenia testu jest znacznie bardziej skomplikowany. Chociażby w wyniku wykorzystania @DataProvider jako dostawcy dla fabryki. Powyższy kod jest oczywiście uproszczony. Fabryki przede wszystkim służą do tworzenia testów, gdzie trzeba użyć konstruktora z parametrami:

Listing 2. Klasy testowe z parametrami w konstruktorach w TestNG

public class FizzBuzzTestNGFactoryConstructorCallTest {

	@Factory
	public Object[] fizzBuzzTestFactory() {
		FizzBuzz sut = new FizzBuzz();
		return Stream.of(
				IntStream.of(3, 6, 99).<Object>mapToObj(p -> new DividedBy3(sut, p)),
				IntStream.of(5, 10, 50).<Object>mapToObj(p -> new DividedBy5(sut, p)),
				IntStream.of(15, 30, 150).<Object>mapToObj(p -> new DividedBy15(sut, p)),
				IntStream.of(2, 8, 11).<Object>mapToObj(p -> new NotDividedBy3Or5(sut, p))
		).flatMap(Function.identity())
				.collect(Collectors.toSet()).toArray();
	}

	static class NotDividedBy3Or5 {

		private final FizzBuzz sut;
		private final int param;

		public NotDividedBy3Or5(FizzBuzz sut, int param) {
			this.sut = sut;
			this.param = param;
		}

		@Test
		public void shouldReturnVal() throws Exception {
			assertEquals(param + "", sut.fizzBuzz(param));
		}
	}

	static class DividedBy5 {


		private final FizzBuzz sut;
		private final int param;

		DividedBy5(FizzBuzz sut, int param) {
			this.sut = sut;
			this.param = param;
		}

		@Test
		public void shouldReturnBuzzIfDiv5() throws Exception {
			assertEquals("Buzz", sut.fizzBuzz(param));
		}
	}

	static class DividedBy3 {
		private final FizzBuzz sut;
		private final int param;

		DividedBy3(FizzBuzz sut, int param) {
			this.sut = sut;
			this.param = param;
		}

		@Test
		public void shouldReturnFizzIfDiv3() throws Exception {
			assertEquals("Fizz", sut.fizzBuzz(param));
		}
	}

	static class DividedBy15 {

		private final FizzBuzz sut;
		private final int param;

		DividedBy15(FizzBuzz sut, int param) {
			this.sut = sut;
			this.param = param;
		}

		@Test
		public void shouldReturnFizzBuzzIfDiv3And5() throws Exception {
			assertEquals("FizzBuzz", sut.fizzBuzz(param));
		}
	}

}

W dodatku rzeczywisty kod może być rozbity na wiele klas w osobnych plikach. To są jednak szczegóły. W JUnit 4 nie było możliwości zrobienia czegoś takiego. JUnit 5 wprowadza mechanizm testów dynamicznych.

Testy dynamiczne

Testy dynamiczne, to testy, które są tworzone w czasie wykonania programu. W połączeniu z mechanizmem fabryk pozwalają one na wygenerowanie testów w locie. Oczywiście coś za coś:

Listing 3. Testy dynamiczne w JUnit 5

public class FizzBuzzJUnit5DynamicTest {

	private FizzBuzz sut = new FizzBuzz();

	@Nested
	class DividedBy15 {

		@TestFactory
		public Collection<DynamicTest> shouldReturnFizzBuzzIfDiv3And5() throws Exception {
			return Arrays.asList(
					dynamicTest("For 15", () -> assertEquals("FizzBuzz", sut.fizzBuzz(15))),
					dynamicTest("For 30", () -> assertEquals("FizzBuzz", sut.fizzBuzz(30))),
					dynamicTest("For 150", () -> assertEquals("FizzBuzz", sut.fizzBuzz(150)))
			);
		}
	}


	@Nested
	class DividedBy5 {

		@TestFactory
		public Stream<DynamicTest> shouldReturnBuzzIfDiv5() throws Exception {
			return Stream.of(5, 10, 50)
					.map(
							val -> dynamicTest(String.format("For %s", val)
									, () -> assertEquals("Buzz", sut.fizzBuzz(val))
							)
					);
		}
	}


	@Nested
	class DividedBy3 {

		@TestFactory
		public Iterable<DynamicTest> shouldReturnFizzIfDiv3() throws Exception {
			return Arrays.asList(
					dynamicTest("for 3", () -> assertEquals("Fizz", sut.fizzBuzz(3))),
					dynamicTest("for 6", () -> assertEquals("Fizz", sut.fizzBuzz(6))),
					dynamicTest("for 99", () -> assertEquals("Fizz", sut.fizzBuzz(99)))
			);
		}
	}

	@Nested
	class NotDividedBy3Or5 {

		@TestFactory
		public Iterator<DynamicTest> shouldReturnVal() throws Exception {
			return Arrays.asList(
					dynamicTest("for 2", () -> assertEquals("2", sut.fizzBuzz(2))),
					dynamicTest("for 9", () -> assertEquals("8", sut.fizzBuzz(8))),
					dynamicTest("for 11", () -> assertEquals("11", sut.fizzBuzz(11)))
			).iterator();
		}
	}
}

Uwaga, używam mechanizmu testów zagnieżdżonych, by wszystko było w jednym miejscu.

Z metody oznaczonej jako @TestFactory, zwracamy kolekcję, iterator, strumień, etc. obiektów DynamicTest. Obiekty te reprezentują poszczególne testy. Wygląda to nieźle, biorąc pod uwagę, że możemy w łatwy sposób np. mapować ResultSet na Stream. To, co odróżnia testy dynamiczne od zwykłych testów, jest inny cykl życia. W przypadku testów dynamicznych nie są uruchamiane metody oznaczone adnotacjami @BeforeEach i @AfterEach. Jest tak, ponieważ klasa DynamicTest, jest tylko wrapperem na implementację Executable, które reprezentuje test jako taki. Executable, jest interfejsem oznaczonym jako @FunctionalInterface. Poza ograniczeniem do pojedynczej metody, nie ma możliwości przekazania zmiennych pomiędzy poszczególnymi lambdami, bez łamania kontraktu.

Jeżeli chcemy użyć @BeforeEach i @AfterEach, to możemy zrobić to na poziomie klasy, w której mamy zdefiniowaną metodę fabrykującą:

Listing 4. Użycie @BeforeEach i @AfterEach z testami dynamicznymi

public class FizzBuzzJUnit5DynamicBeforeTest {

	@Nested
	class DividedBy15 {

		private FizzBuzz sut;

		@BeforeEach
		public void setUp() {
			sut = new FizzBuzz();
		}

		@TestFactory
		public Collection<DynamicTest> shouldReturnFizzBuzzIfDiv3And5() throws Exception {
			return Arrays.asList(
					dynamicTest("For 15", () -> assertEquals("FizzBuzz", sut.fizzBuzz(15))),
					dynamicTest("For 30", () -> assertEquals("FizzBuzz", sut.fizzBuzz(30))),
					dynamicTest("For 150", () -> assertEquals("FizzBuzz", sut.fizzBuzz(150)))
			);
		}
	}


	@Nested
	class DividedBy5 {
		private FizzBuzz sut;

		@BeforeEach
		public void setUp() {
			sut = new FizzBuzz();
		}

		@TestFactory
		public Stream<DynamicTest> shouldReturnBuzzIfDiv5() throws Exception {
			return Stream.of(5, 10, 50)
					.map(
							val -> dynamicTest(String.format("For %s", val)
									, () -> assertEquals("Buzz", sut.fizzBuzz(val))
							)
					);
		}
	}


	@Nested
	class DividedBy3 {
		private FizzBuzz sut;

		@BeforeEach
		public void setUp() {
			sut = new FizzBuzz();
		}

		@TestFactory
		public Iterable<DynamicTest> shouldReturnFizzIfDiv3() throws Exception {
			return Arrays.asList(
					dynamicTest("for 3", () -> assertEquals("Fizz", sut.fizzBuzz(3))),
					dynamicTest("for 6", () -> assertEquals("Fizz", sut.fizzBuzz(6))),
					dynamicTest("for 99", () -> assertEquals("Fizz", sut.fizzBuzz(99)))
			);
		}
	}

	@Nested
	class NotDividedBy3Or5 {
		private FizzBuzz sut;

		@BeforeEach
		public void setUp() {
			sut = new FizzBuzz();
		}

		@TestFactory
		public Iterator<DynamicTest> shouldReturnVal() throws Exception {
			return Arrays.asList(
					dynamicTest("for 2", () -> assertEquals("2", sut.fizzBuzz(2))),
					dynamicTest("for 9", () -> assertEquals("8", sut.fizzBuzz(8))),
					dynamicTest("for 11", () -> assertEquals("11", sut.fizzBuzz(11)))
			).iterator();
		}
	}
}

Przy czym metody te zostaną wywołane raz, przed wywołaniem metody fabrykującej.

Podsumowanie

Mechanizm testów dynamicznych w JUnit 5 działa inaczej niż fabryki z TestNG. Ma on pewne ograniczenia, ale nie są one na tyle duże, by całkowicie przekreślić ten mechanizm.

JUnit 5 – Testy parametryzowane

Sama możliwość wielokrotnego uruchomienia testu, to nie wszystko. W pewnych przypadkach chcielibyśmy, by nasz test został uruchomiony dla różnych zestawów danych. Ma to sens, jeżeli nasze dane wejściowe reprezentują pewien spójny podzbiór przypadków testowych. Mówiąc inaczej mamy dane, które mają sprawdzić jedną ze ścieżek.

Listing 1. Każdy test sprawdza jedną ze ścieżek – JUnit 4

public class FizzBuzzJUnit4WithoutRunnersTest {

	private FizzBuzz sut;
	
	@Before
	public void setup() {
		sut = new FizzBuzz();
	}

	@Test
	public void shouldReturnFizzBuzzIfDiv3And5() throws Exception {
		assertEquals("FizzBuzz", sut.fizzBuzz(15));
		assertEquals("FizzBuzz", sut.fizzBuzz(30));
		assertEquals("FizzBuzz", sut.fizzBuzz(150));
	}

	@Test
	public void shouldReturnBuzzIfDiv5() throws Exception {
		assertEquals("Buzz", sut.fizzBuzz(5));
		assertEquals("Buzz", sut.fizzBuzz(10));
		assertEquals("Buzz", sut.fizzBuzz(50));
	}

	@Test
	public void shouldReturnFizzIfDiv3() throws Exception {
		assertEquals("Fizz", sut.fizzBuzz(3));
		assertEquals("Fizz", sut.fizzBuzz(6));
		assertEquals("Fizz", sut.fizzBuzz(99));

	}
	@Test
	public void shouldReturnVal() throws Exception {
		assertEquals("2", sut.fizzBuzz(2));
		assertEquals("8", sut.fizzBuzz(8));
		assertEquals("11", sut.fizzBuzz(11));

	}
}

Ten kod ma dość dużą powtarzalność. Poszczególne testy odpowiadają, za jeden przypadek, ale kod duplikuje się. Używając JUnit 4, możemy, to poprawić za pomocą odpowiedniego runnera.

Listing 2. Testy parametryzowane w JUnit 4

@RunWith(JUnitParamsRunner.class)
public class FizzBuzzJUnit4WithRunnersTest {

	private FizzBuzz sut;

	@Before
	public void setup() {
		sut = new FizzBuzz();
	}

	@Test
	@Parameters(value = {"15", "30", "150"})
	public void shouldReturnFizzBuzzIfDiv3And5(int p) throws Exception {
		assertEquals("FizzBuzz", sut.fizzBuzz(p));
	}

	@Test
	@Parameters(value = {"5", "10", "50"})
	public void shouldReturnBuzzIfDiv5(int p) throws Exception {
		assertEquals("Buzz", sut.fizzBuzz(p));
	}

	@Test
	@Parameters(value = {"3", "6", "99"})
	public void shouldReturnFizzIfDiv3(int p) throws Exception {
		assertEquals("Fizz", sut.fizzBuzz(p));
	}


	@Test
	@Parameters(value = {"2", "8", "11"})
	public void shouldReturnVal(int p) throws Exception {
		assertEquals("" + p, sut.fizzBuzz(p));
	}
}

Już jest zdecydowanie lepiej.

Uwaga, choć metody testowe nadal wyglądają bardzo podobnie i można by brnąć w kolejną refaktoryzację, to każda z nich dotyczy innego warunku. Nie należy pisać ubertestu, który z wykorzystaniem parametrów, będzie obsługiwać wszystkie przypadki.

Tyle tylko, że wpadliśmy z deszczu pod rynnę, lub jak kto woli z COBOLa w phpa. Największą wadą runnerów JUnit 4, jest brak możliwości łączenia kliku z nich. Zatem do naszego testu nie dołączymy już na przykład runnera Springa.

Jeszcze inaczej do problemu podchodzi TestNG. Tu mamy do dyspozycji mechanizm związany z podawaniem parametrów z pliku testng.xml oraz mechanizm związany z adnotacją @DataProvider i własnością dataProvider w @Test:

Listing 3. Testy parametryzowane w TestNG

public class FizzBuzzTestNGParametrizedTest {

    private FizzBuzz sut;

    @BeforeTest
    public void setup() {
        sut = new FizzBuzz();
    }

    @Test(dataProvider = "3 and 5")
    public void shouldReturnFizzBuzzIfDiv3And5(int p) throws Exception {
        assertEquals("FizzBuzz", sut.fizzBuzz(p));
    }

    @Test(dataProvider = "5 only")
    public void shouldReturnBuzzIfDiv5(int p) throws Exception {
        assertEquals("Buzz", sut.fizzBuzz(p));
    }

    @Test(dataProvider = "3 only")
    public void shouldReturnFizzIfDiv3(int p) throws Exception {
        assertEquals("Fizz", sut.fizzBuzz(p));
    }

    @Test(dataProvider = "other values")
    public void shouldReturnVal(int p) throws Exception {
        assertEquals(p + "", sut.fizzBuzz(p));
    }

    @DataProvider(name = "3 and 5")
    public Object[][] data3And5() {
        return new Object[][]{
                new Object[]{15},
                new Object[]{30},
                new Object[]{150}
        };
    }

    @DataProvider(name = "3 only")
    public Object[][] data3Only() {
        return new Object[][]{
                new Object[]{3},
                new Object[]{6},
                new Object[]{99}
        };
    }

    @DataProvider(name = "5 only")
    public Object[][] data5Only() {
        return new Object[][]{
                new Object[]{5},
                new Object[]{10},
                new Object[]{50}
        };
    }

    @DataProvider(name = "other values")
    public Object[][] dataOtherValues() {
        return new Object[][]{
                new Object[]{2},
                new Object[]{8},
                new Object[]{11}
        };
    }
}

Mechanizm ten jest dość elastyczny. Jeżeli chcemy zdefiniować dostawcę w innej klasie, to musi być on statyczną metodą w klasie z konstruktorem bezargumentowym. Dodatkowo podajemy jeszcze dataProviderClass. Nadal jednak musimy samodzielnie zaimplementować pobieranie danych z zewnętrznych źródeł np. plików csv.

Testy parametryzowane w JUnit 5

W JUnit 5 w M4 wprowadzono możliwość tworzenia testów parametryzowanych. Wymaga to od nas trochę zachodu, bo na początku musimy dodać odpowiedni moduł do naszej konfiguracji, ale efekty są świetne.

Konfiguracja

Obsługa testów parametryzowanych została wydzielona do osobnego modułu. By z niego skorzystać musimy zmodyfikować konfigurację w mavenie, dodając odpowiednią zależność:

Listing 4. Dodatkowa zależność

    
        
            org.junit.jupiter
            junit-jupiter-params
            5.0.0-M4
        
    

Dodanie zależności do modułu junit-jupiter-params otwiera na drogę do pisania testów parametrzowanych. Samo pisanie testów wymaga od nas podania adnotacji @ParametrizedTest oraz odpowiedniej adnotacji określającej źródło parametrów. Adnotacja ta jest traktowana jak adnotacja @Test.

Istnieje kilka możliwości podania parametrów. Przyjrzymy się tym, które wydają się najprzydatniejsze. O przydatności metody decyduje moje doświadczenie z TestNG.

Parametry zaszyte w kodzie

Najprostszą metodą jest zaszycie parametrów bezpośrednio w kodzie, za pomocą adnotacji @ValueSource. Adnotacja ta może przyjąć listę int-ów albo String-ów.

Listing 5. Parametryzacja za pomocą @ValueSource

public class FizzBuzzJUnit5ParametrizedValueSourceTest {

    private FizzBuzz sut;

    @BeforeEach
    public void setup() {
        sut = new FizzBuzz();
    }

    @ParameterizedTest
    @ValueSource(ints = {15, 30, 150})
    public void shouldReturnFizzBuzzIfDiv3And5(int p) throws Exception {
        assertEquals("FizzBuzz", sut.fizzBuzz(p));
    }

    @ParameterizedTest
    @ValueSource(ints = {5, 10, 50})
    public void shouldReturnBuzzIfDiv5(int p) throws Exception {
        assertEquals("Buzz", sut.fizzBuzz(p));
    }

    @ParameterizedTest
    @ValueSource(ints = {3, 6, 99})
    public void shouldReturnFizzIfDiv3(int p) throws Exception {
        assertEquals("Fizz", sut.fizzBuzz(p));
    }

    @ParameterizedTest
    @ValueSource(ints = {2, 8, 11})
    public void shouldReturnVal(int p) throws Exception {
        assertEquals(p + "", sut.fizzBuzz(p));
    }
}

Metoda ta jest wygodna dla niewielkiego zbioru danych.

Metoda fabrykujaca

Kolejnym sposobem na podanie argumentów do naszego testu jest użycie metody fabrykującej. Działa to na tej samej zasadzie co dostawcy w TestNG. Wystarczy w adnotacji @MethodSource wskazać na metodę z tej samej klasy:

Listing 6. Metoda jako źródło danych

public class FizzBuzzJUnit5ParametrizedMethodSourceTest {

    private FizzBuzz sut;

    @BeforeEach
    public void setup() {
        sut = new FizzBuzz();
    }

    @ParameterizedTest
    @MethodSource(names = "data3And5")
    public void shouldReturnFizzBuzzIfDiv3And5(int p) throws Exception {
        assertEquals("FizzBuzz", sut.fizzBuzz(p));
    }

    @ParameterizedTest
    @MethodSource(names = "data5Only")
    public void shouldReturnBuzzIfDiv5(int p) throws Exception {
        assertEquals("Buzz", sut.fizzBuzz(p));
    }

    @ParameterizedTest
    @MethodSource(names = "data3Only")
    public void shouldReturnFizzIfDiv3(int p) throws Exception {
        assertEquals("Fizz", sut.fizzBuzz(p));
    }

    @ParameterizedTest
    @MethodSource(names = "dataOtherValues")
    public void shouldReturnVal(int p) throws Exception {
        assertEquals(p + "", sut.fizzBuzz(p));
    }

    static Stream data3And5() {
        return Stream.of(15, 30, 150);
    }

    static Stream data3Only() {
        return Stream.of(3, 6, 99);
    }

    static Stream data5Only() {
        return Stream.of(5, 10, 50);
    }

    static Stream dataOtherValues() {
        return Stream.of(2, 8, 11);
    }
}

Należy tu zwrócić uwagę na dwie rzeczy. Pierwsza to konieczność wykorzystania klasy Stream do produkcji danych. Niestety w przypadku próby użycia np. IntStream otrzymamy błąd w czasie uruchomienia testów. Druga to konieczność wykorzystania metod statycznych.

Dostawcy

Wykorzystując metody, jesteśmy ograniczeni do tych, które są w klasie testowej. Jeżeli chcemy zdefiniować źródło danych w osobnej klasie, to musimy wykorzystać adnotację @ArgumentSource. Jako parametr przyjmuje ona klasę, która implementuje interfejs ArgumentsProvider. Interfejs ten ma jedną metodę arguments, w której mamy dostęp do ContainerExtensionContext. Co to jest, kiedy indziej.

Listing 7. Wykorzystanie klasy jako dostawcy

public class FizzBuzzJUnit5ParametrizedArgumentSourceTest {

    private FizzBuzz sut;

    @BeforeEach
    public void setup() {
        sut = new FizzBuzz();
    }

    @ParameterizedTest
    @ArgumentsSource(Data3And5.class)
    public void shouldReturnFizzBuzzIfDiv3And5(int p) throws Exception {
        assertEquals("FizzBuzz", sut.fizzBuzz(p));
    }

    @ParameterizedTest
    @ArgumentsSource(Data5Only.class)
    public void shouldReturnBuzzIfDiv5(int p) throws Exception {
        assertEquals("Buzz", sut.fizzBuzz(p));
    }

    @ParameterizedTest
    @ArgumentsSource(Data3Only.class)
    public void shouldReturnFizzIfDiv3(int p) throws Exception {
        assertEquals("Fizz", sut.fizzBuzz(p));
    }

    @ParameterizedTest
    @ArgumentsSource(DataOtherValues.class)
    public void shouldReturnVal(int p) throws Exception {
        assertEquals(p + "", sut.fizzBuzz(p));
    }

    static class Data3And5 implements ArgumentsProvider {
        @Override
        public Stream<? extends Arguments> arguments(ContainerExtensionContext containerExtensionContext) throws Exception {
            return Stream.of(15, 30, 150).map(ObjectArrayArguments::create);
        }
    }

    static class Data3Only implements ArgumentsProvider {
        @Override
        public Stream<? extends Arguments> arguments(ContainerExtensionContext containerExtensionContext) throws Exception {
            return Stream.of(3, 6, 99).map(ObjectArrayArguments::create);
        }
    }

    static class Data5Only implements ArgumentsProvider {
        @Override
        public Stream<? extends Arguments> arguments(ContainerExtensionContext containerExtensionContext) throws Exception {
            return Stream.of(5, 10, 50).map(ObjectArrayArguments::create);
        }
    }

    static class DataOtherValues implements ArgumentsProvider {
        @Override
        public Stream<? extends Arguments> arguments(ContainerExtensionContext containerExtensionContext) throws Exception {
            return Stream.of(2, 8, 11).map(ObjectArrayArguments::create);
        }
    }
}

Tu użyłem wewnętrznej klasy statycznej, ale tylko po to, by całość mieściła się na jednym listingu. Wygląda to już całkiem dobrze.

Format CSV

Kolejnym rodzajem źródeł danych jest format CSV. Możemy go użyć na dwa sposoby. Pierwszy z nich jest obsługiwany za pomocą adnotacji @CsvSource. Działa ona na tej samej zasadzie co @ValueSource. Jako wartość przyjmuje tablicę String-ów. Jeden String jeden rekord.

Listing 8. Format CSV w pliku źródłowym

public class FizzBuzzJUnit5ParametrizedCsvSourceTest {

    private FizzBuzz sut;

    @BeforeEach
    public void setup() {
        sut = new FizzBuzz();
    }

    @ParameterizedTest
    @CsvSource({"15", "30", "150"})
    public void shouldReturnFizzBuzzIfDiv3And5(int p) throws Exception {
        assertEquals("FizzBuzz", sut.fizzBuzz(p));
    }

    @ParameterizedTest
    @CsvSource({"5", "10", "50"})
    public void shouldReturnBuzzIfDiv5(int p) throws Exception {
        assertEquals("Buzz", sut.fizzBuzz(p));
    }

    @ParameterizedTest
    @CsvSource({"3", "6", "99"})
    public void shouldReturnFizzIfDiv3(int p) throws Exception {
        assertEquals("Fizz", sut.fizzBuzz(p));
    }

    @ParameterizedTest
    @CsvSource({"2", "8", "11"})
    public void shouldReturnVal(int p) throws Exception {
        assertEquals(p + "", sut.fizzBuzz(p));
    }
}

Format ten będzie sprawdzać się, gdy nie chcemy tworzyć osobnych dostawców. Jedna format CSV, to przede wszystkim pliki. Do obsługi plików w tym formacie jest dedykowana adnotacja @CsvFileSource.

Listing 9. Obsługa plików CSV

public class FizzBuzzJUnit5ParametrizedCsvFileSourceTest {

    private FizzBuzz sut;

    @BeforeEach
    public void setup() {
        sut = new FizzBuzz();
    }

    @ParameterizedTest
    @CsvFileSource(resources = "/data3And15.csv")
    public void shouldReturnFizzBuzzIfDiv3And5(int p) throws Exception {
        assertEquals("FizzBuzz", sut.fizzBuzz(p));
    }

    @ParameterizedTest
    @CsvFileSource(resources = "/data5Only.csv")
    public void shouldReturnBuzzIfDiv5(int p) throws Exception {
        assertEquals("Buzz", sut.fizzBuzz(p));
    }

    @ParameterizedTest
    @CsvFileSource(resources = "/data3Only.csv")
    public void shouldReturnFizzIfDiv3(int p) throws Exception {
        assertEquals("Fizz", sut.fizzBuzz(p));
    }

    @ParameterizedTest
    @CsvFileSource(resources = "/dataOtherValues.csv")
    public void shouldReturnVal(int p) throws Exception {
        assertEquals(p + "", sut.fizzBuzz(p));
    }
}

Ścieżka liczona jest w tym przypadku od katalogu test/resources w mavenie.

Inne źródła

Używając adnotacji @EnumSource możemy potraktować typ wyliczeniowy jako źródło danych. JUnit 5 potrafi też samodzielnie dokonać konwersji pomiędzy String, a podstawowymi typami takimi jak liczny, enumy czy daty w różnych odmianach.

Konwerery

Korzystając z różnych źródeł danych, staniemy przed problemem konwersji do jakiegoś złożonego typu. Przykładowo odczytując dane z pliku CSV, chcemy zamienić pojedynczy rekord na opis transakcji. W innym przypadku dane ze źródła mogą być w innym formacie niż oczekiwany.

W takim wypadku musimy napisać własny konwerter oraz określić, że parametr testu ma zostać skonwertowany.

Listing 10. Test z konwersją HEX na DEC

public class FizzBuzzJUnit5ParametrizedConvertersTest {

    private FizzBuzz sut;

    @BeforeEach
    public void setup() {
        sut = new FizzBuzz();
    }

    @ParameterizedTest
    @ValueSource(strings = {"f", "1E", "96"})
    public void shouldReturnFizzBuzzIfDiv3And5(@ConvertWith(HexToInt.class) int p) throws Exception {
        assertEquals("FizzBuzz", sut.fizzBuzz(p));
    }

    @ParameterizedTest
    @ValueSource(strings = {"5", "a", "32"})
    public void shouldReturnBuzzIfDiv5(@ConvertWith(HexToInt.class) int p) throws Exception {
        assertEquals("Buzz", sut.fizzBuzz(p));
    }

    @ParameterizedTest
    @ValueSource(strings = {"3", "6", "63"})
    public void shouldReturnFizzIfDiv3(@ConvertWith(HexToInt.class) int p) throws Exception {
        assertEquals("Fizz", sut.fizzBuzz(p));
    }

    @ParameterizedTest
    @ValueSource(strings = {"2", "8", "B"})
    public void shouldReturnVal(@ConvertWith(HexToInt.class) int p) throws Exception {
        assertEquals(p + "", sut.fizzBuzz(p));
    }

    static class HexToInt extends SimpleArgumentConverter {
        @Override
        protected Object convert(Object o, Class<?> targetType) {
            assertEquals(int.class, targetType, "Can only convert to int");
            return Integer.decode("0x" + o.toString());
        }
    }
}

Nasz konwerter musi implementować interfejs ArgumentConverter. Tu wykorzystuję klasę SimpleArgumentConverter, która wyciąga typ docelowy z parametru ParameterContext. Następnie argument testu należy oznaczyć jako @ConvertWith i już możemy się cieszyć.

Podsumowanie

To co mnie urzekło w obsłudze testów parametryzowanych w JUnit 5, to ogromna elastyczność. Twórcy tego rozwiązania wzięli pod uwagę doświadczenia użytkowników. Wybrali najpopularniejsze sposoby użycia mechanizmu parametrów, po czym zaimplementowali je w module. Brawo.