10 lat minęło

Kilka dni temu minęło 10 lat od momentu, kiedy napisałem pierwszy post na bloga. Strasznie długo już się tym zajmuję. Przecież niedawno świętowaliśmy 1000 post.

Dziękuję wszystkim czytelnikom 🙂

JUnit 5 – Testy powtarzalne

Jedną z cech testów jednostkowych jest ich powtarzalność. Najlepiej, jeżeli powtórzenie jest wykonywane automatycznie. Jednak nie o tym dziś będziemy mówić. Czasami pisząc testy, musimy uwzględnić, że nasz kod uruchamiany jest w środowisku wielowątkowym. Nie ma w tym nic nadzwyczajnego. Podobnie jest w przypadku, gdy tworzymy testy, których zadaniem jest jakiegoś kodu w wielu kopiach jednocześnie. Dla mnie wzorcową implementacją tego zachowania jest TestNG:

Listing 1. Testy powtarzalne w TestNG

@Test
public class FizzBuzzTestNGRepeatedTest {

	private FizzBuzz sut;

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

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

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

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

	@Test(invocationCount = 100, threadPoolSize = 4)
	public void shouldReturnVal() throws Exception {
		assertEquals("2", sut.fizzBuzz(2));
	}

}

Mamy tu dość dużą elastyczność w konfiguracji testów. Po pierwsze za pomocą parametru invocationCount, możemy określić, ile testów ma zostać wykonanych. Po drugie parametr threadPoolSize pozwala nam na skonfigurowanie liczby wątków użytych do uruchomienia testów.

W przypadku JUnit 5 wygląda to trochę inaczej. Po pierwsze wprowadzono adnotację @RepeatedTest, która pozwala na określenie liczby powtórzeń. Adnotacja ta zachowuje się jak specjalizowana @Test.

Listing 2. Testy powtarzalne w JUnit 5

public class FizzBuzzJUnit5RepeatedTest {

	private FizzBuzz sut;

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

	@RepeatedTest(value = 100, name = "Repetition {currentRepetition} of {totalRepetition}")
	public void shouldReturnFizzBuzzIfDiv3And5() throws Exception {
		assertEquals("FizzBuzz", sut.fizzBuzz(15));
	}

	@RepeatedTest(value = 100, name = "Repetition {currentRepetition} of {totalRepetition}")
	public void shouldReturnBuzzIfDiv5() throws Exception {
		assertEquals("Buzz", sut.fizzBuzz(5));
	}

	@RepeatedTest(value = 100, name = "Repetition {currentRepetition} of {totalRepetition}")
	public void shouldReturnFizzIfDiv3() throws Exception {
		assertEquals("Fizz", sut.fizzBuzz(3));
	}

	@RepeatedTest(value = 100, name = "Repetition {currentRepetition} of {totalRepetition}")
	public void shouldReturnVal() throws Exception {
		assertEquals("2", sut.fizzBuzz(2));
	}
}

Po drugie, adnotacja ta ma parametr name, który pozwala na dodanie nazwy specyficznej dla uruchomienia. W nazwie tej możemy wykorzystać trzy flagi:

  • {currentRepetition} – w której jest numer aktualnego przebiegu,
  • {totalRepetition} – w której mamy ilość wszystkich przebiegów,
  • {displayName} – która zawiera nazwę testu zdefiniowaną w @DisplayName.

Możemy też jako wartość parametru podać jedną ze stałych zdefiniowanych w samej adnotacji:

  • SHORT_DISPLAY_NAME – która jest równoważna „repetition {currentRepetition} of {totalRepetition}”,
  • LONG_DISPLAY_NAME – która jest równoważna „{displayName} :: repetition {currentRepetition} of {totalRepetition}”.

Problem polega jednak na tym, że nie możemy skonfigurować liczby wątków, które zostaną wykorzystane do uruchomienia testu. Nie pozostaje nic innego jak użycie specyficznego silnika, którego oczywiście jeszcze nie ma.

JUnit 5 – Zagnieżdżanie testów

Samo nazywanie i tagowanie testów nie zawsze jest wystarczające. Szczególnie że oba te mechanizmy są stosunkowo proste. Pozwalają one na organizację testów w pewien logiczny sposób. Nazwy pozwalają na odszukiwanie testów w raportach, z wykorzystaniem przeszukiwania pełnotekstowego. Tagi pozwalają na oznaczanie testów o podobnym znaczeniu w ramach całej aplikacji. Pozwala to na filtrowanie i wybiórcze uruchamianie grup testów. Nadal jednak brakuje nam mechanizmu, który pozwoliłby na na grupowanie testów na na poziomie struktury kodu. JUnit 5 wprowadza taki mechanizm.

Testy zagnieżdżone

Java pozwala na tworzenie klas wewnętrznych. Klasy te mogą być zarówno statyczne, jak i niestatyczne. Obiektu klasy statycznej jest powiązana z klasą, w której została zdefiniowana – klasą zewnętrzną. W przypadku klasy niestatycznej potrzebujemy obiekt klasy zewnętrznej, do którego dopięty będzie obiekt klasy wewnętrznej. I właśnie ten mechanizm jest wykorzystywany przez JUnit 5. Oczywiście nic za darmo. Musimy dodać adnotację @Nested do naszej klasy wewnętrznej. Klasa ta musi być niestatyczna. W przeciwnym wypadku, testy zostaną zignorowane.

Listing 1. Testy zagnieżdżone

@DisplayName("FizzBuzz should")
public class FizzBuzzJUnit5NestedTest {

	private FizzBuzz sut;

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

	@Nested
	@DisplayName("return FizzBuzz when dividable by 3 and 5")
	class DividedBy15 {

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


	@Nested
	@DisplayName("return Buzz when dividable by 5")
	class DividedBy5 {
		@Test
		public void shouldReturnBuzzIfDiv5() throws Exception {
			assertEquals("Buzz", sut.fizzBuzz(5));
		}
	}


	@Nested
	@DisplayName("return Fizz when dividable by 3")
	class DividedBy3 {

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

	@Nested
	@DisplayName("return number in other cases")
	class NotDividedBy3Or5 {

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

Cały „power” tego rozwiązania jest widoczny w połączeniu z nazwaniem testów. Przy czym problem na chwilę obecną polega na tym, że po aktualizacji JUnit 5 do milestone 4, Idea nie potrafi uruchomić testów 🙁 Skazuje nas, to na walkę z konsolą. W wersji M3 jeszcze wszystko działa. Cóż, takie uroki używania softu, który jest w fazie produkcji.

JUnit 5 – nazywanie testów, tagi i filtrowanie

Jedną z najbardziej rozpoznawalnych cech BDDW jest opisowość testów. Oczywiście nie jest to najważniejsza cecha. Raczej jest to produkt uboczny metodyki. Jednak gdy piszemy testy, to taka opisowość jest bardzo przydatna. Zresztą widać, że narzędzia pozwalające na opisywanie testów w bardziej naturalny sposób zyskują popularność w ostatnich latach.

Jedną z rzeczy, których brakuje w JUnit 4, jest możliwość zdefiniowania opisu dla testu. Niestety zaszycie opisu w nazwie metody jest słabe. Jesteśmy ograniczeni przez język, a dodatkowo ciężko czyta się camel case. O notacji z podkreśleniami nie wspominam, bo oznacza to dodatkową zabawę z checkstyle. TestNG pozwalało nazywać testy, za pośrednictwem pola testName w adnotacji @Test:

Listing 1. Nazywanie testów w TestNG

@Test(suiteName = "FizzBuzz should")
public class FizzBuzzTestNGNamedTest {

	private FizzBuzz sut;

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

	@Test(testName = "return FizzBuzz when dividable by 3 and 5")
	public void shouldReturnFizzBuzzIfDiv3And5() throws Exception {
		assertEquals("FizzBuzz", sut.fizzBuzz(15));
	}

	@Test(testName = "return Buzz when dividable by 5")
	public void shouldReturnBuzzIfDiv5() throws Exception {
		assertEquals("Buzz", sut.fizzBuzz(5));
	}

	@Test(testName = "return Fizz when dividable by 3")
	public void shouldReturnFizzIfDiv3() throws Exception {
		assertEquals("Fizz", sut.fizzBuzz(3));
	}

	@Test(testName = "return number in other cases")
	public void shouldReturnVal() throws Exception {
		assertEquals("2", sut.fizzBuzz(2));
	}
}

Mechanizm ten nie jest jednak kompletny. Na przykład IntelliJ wyświetla nazwy metod, a nie testów. JUnit 5 wprowadza dwa inne mechanizmy.

Testy nazwane

Najprostszym mechanizmem jest nazwanie testów. Służy do tego specjalna adnotacja @DisplayName. Znowuż, nazwa adnotacji świetnie oddaje intencje. Wyświetlamy test pod pewną nazwą. Jego rzeczywista nawa, to nazwa metody 🙂

Listing 2. Użycie @DisplayName

@DisplayName("FizzBuzz should")
public class FizzBuzzJUnit5DescriptiveTest {

	private FizzBuzz sut;

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

	@Test
	@DisplayName("return FizzBuzz when dividable by 3 and 5")
	public void shouldReturnFizzBuzzIfDiv3And5() throws Exception {
		assertEquals("FizzBuzz", sut.fizzBuzz(15));
		assertEquals("FizzBuzz", sut.fizzBuzz(30));
		assertEquals("FizzBuzz", sut.fizzBuzz(150));
	}

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

	@Test
	@DisplayName("return Fizz when dividable by 3")
	public void shouldReturnFizzIfDiv3() throws Exception {
		assertEquals("Fizz", sut.fizzBuzz(3));
		assertEquals("Fizz", sut.fizzBuzz(6));
		assertEquals("Fizz", sut.fizzBuzz(99));
	}

	@Test
	@DisplayName("return number in other cases")
	public void shouldReturnVal() throws Exception {
		assertEquals("2", sut.fizzBuzz(2));
		assertEquals("8", sut.fizzBuzz(8));
		assertEquals("11", sut.fizzBuzz(11));
	}
}

Oczywiście same nazwy to nie wszystko. Pozwalają one zaszaleć na poziomie generowania raportów, ale dopiero w połączeniu z tagami możemy odfiltrować testy.

Tagi i filtrowanie

Filtrowanie testów zarówno w JUnit 4 jak i w TestNG odbywało się na podstawie nazwy klasy. Dodatkowo TestNG udostępniał mechanizm grup testów, które pozwalały na zbieranie testów razem:

Listing 3. Mechanizm grup z TestNG

public class FizzBuzzTestNGGroupedTest {

	private FizzBuzz sut;

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

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

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

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

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

Test z grupy 3 i grupy 5 można uruchomić oddzielnie. Możemy też uzależnić uruchomienie pewnych testów od tego czy testy z jakiejś grupy przeszły. To jest bardzo potężny mechanizm. JUnit 5 podszedł do problemu trochę inaczej. Wprowadzając adnotację @Tag i dając możliwość, filtrowania testów po tej adnotacji.

Listing 4. Mechanizm tagów z JUnit 5

public class FizzBuzzJUnit5TagsTest {

	private FizzBuzz sut;

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

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

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

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

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

Proces filtrowania odbywa się w trakcie wykrywania testów obecnych w classpath. Zatem o ich uruchomieniu lub nie, decydujemy na poziomie silnika testów. Oczywiście, jeżeli chcemy użyć mavena, by uruchomić tylko wybrane testy, to możemy to zrobić:

Listing 5. Konfiguracja mavena


    maven-surefire-plugin
    2.19
    
        
            3
        
    

Przy czym, mechanizm ten nie jest aż tak elastyczny jak grupy w TestNG. Jeżeli jednak dodamy odpowiedni silnik…

JUnit 5 – Pierwsze kroki

Dziś pobawimy się najprostszymi testami JUnit 5. Samo tworzenie i uruchamianie testów nie różni się w znaczący od wersji 4. Przynajmniej z punktu widzenia użytkownika. Tworzymy test, odpalamy mavena albo też naciskamy Alt+Shift+F10 Enter i gotowe. Jest jednak kilka drobnych, acz znaczących różnic, jeżeli chcemy skorzystać z czegoś więcej. Na przykład przygotować konfiguracje do testu.

Test referencyjny w JUnit 4

Poniżej test JUnit 4, który stanowi punkt odniesienia dla kodu pisanego z użyciem JUnit 5. Nie jest to najlepszy możliwy test, ale nie chciałem zaśmiecać go niepotrzebnymi elementami np. dodatkowymi asercjami.

Listing 1. Testy napisane z użyciem JUnit4

public class FizzBuzzJUnit4WithoutRunnersTest {

	private FizzBuzz sut;

	@BeforeClass
	public static void classSetup() {
		Logger.getLogger("JUnit 4").info("Started at " + LocalDateTime.now());
	}

	@AfterClass
	public static void classTeardown() {
		Logger.getLogger("JUnit 4").info("Finished at " + LocalDateTime.now());
	}

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

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

	@Test
	public void shouldReturnBuzzIfDiv5() throws Exception {
		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));
	}
}

Mamy tu cztery metody testowe oraz metodę setup, która będzie wykonywać się przed każdym testem. Dodatkowo mamy dwie metody classSetup i classTeardown, które są uruchamiane przed utworzeniem instancji klasy testowej. Co dokładnie tu się dzieje, to nie jest istotne.

Zmiany w JUnit 5

JUnit 5 wprowadził kilka drobnych zmian w nazewnictwie adnotacji. Nowe adnotacje znacznie lepiej opisują, co się dzieje. Nasza klasa testowa po traktowaniu JUnit 5 będzie wyglądać w następujący sposób:

Listing 2. Testy napisane z użyciem JUnit 5

public class FizzBuzzJUnit5Test {

	private FizzBuzz sut;

	@BeforeAll
	static void classSetup() {
		Logger.getLogger("JUnit 4").info("Started at " + LocalDateTime.now());
	}

	@AfterAll
	static void classTeardown() {
		Logger.getLogger("JUnit 4").info("Finished at " + LocalDateTime.now());
	}

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

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

	@Test
	public void shouldReturnBuzzIfDiv5() throws Exception {
		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));
	}
}

I tak oto adnotacja @Before została zastąpiona przez @BeforeEach, a @BeforeClass przez @BeforeAll. Podobnie ma się sprawa z adnotacjami @After*

Oczywiście nie jest to jedyna zmiana.

Wyłączanie testów

W JUnit 4 wyłączenie testu polegało na dodaniu adnotacji @Ignore:

Listing 3. Przykład wyłączonego testu JUnit 4

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

O ile w przypadku adnotacji @Before*/@After* można było domyślić się jaka jest intencja użytkownika, to już w przypadku adnotacji @Ignore, jest znacznie gorzej.

Test ignorowany, czyli co?

Kilka razy miałem w różnych zespołach rozmowę na ten temat. Czy jeżeli test jest ignorowany, to można go wywalić? Czy oznacza to, że można zignorować jego rezultaty? A może coś jeszcze innego? Zazwyczaj kończyło się wdrożeniem kolejnego, świetnego, i oczywistego schematu nazywania testów, który umożliwia ich filtrowanie. Żeby było zabawniej, schematy nazewnicze średnio działają z metodami, co prowadzi do tworzenia „kontenerów na testy ignorowane”. Klas, które zawierają testy, ale wszystkie są wyłączone… no właśnie, wyłączone.

Oczywiście intencja programisty, który użył tej adnotacji jest zupełnie inna. Chce on wyłączyć test. Z jakiegoś powodu test nie powinien być uruchamiany. Nie oznacza to, że będzie on pomijany. Po prostu w danym momencie chcemy nie uruchamiać danego testu. W tym celu możemy użyć adnotacji @Disable:

Listing 4. Przykład wyłączonego testu JUnit 5

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

Intencja jest teraz oczywista. Test został wyłączony.

Podsumowanie

JUnit 5 nie zmienia sposobu pisania testów w jakiś drastyczny sposób. Nie na tym podstawowym, prostym poziomie. Zmiany, które wprowadza, są subtelniejsze. Nowe nazewnictwo znacznie lepiej opisuje intencje stojące za poszczególnymi fragmentami kodu. Tym samym ułatwia nam czytanie testów i ich zrozumienie. W połączeniu z innymi mechanizmami, które omówię w kolejnych wpisach, daje nam nowe możliwości w tworzeniu kodu testowego.

JUnit 5 – Wstęp

JUnit 4 jest już stary. Został opublikowany gdzieś w okolicach 2006 roku. Ostatnia aktualizacja (4.12) to rok 2014. Sama biblioteka nie jest zła, jeśli chodzi o testy jednostkowe. Spełnia swoje zadanie i nie ma powodu by się do czegoś przyczepić. Przez ostatnie 11 lat zaszło trochę zmian w języku jak i w sposobach wytwarzania oprogramowania. Przyszedł już czas na aktualizację.

Rzeczy które w JUnit 4 mi nie pasują

JUnit jest świetny, jeśli chodzi o testy jednostkowe. I tylko jeśli chodzi o testy jednostkowe. Jakakolwiek próba użycia go w testach integracyjnych jest z góry skazana na niepowodzenie. JUnit został zaprojektowany tak, by testy w nim pisane były niezależne, szybkie, powtarzalne i automatyzowalne. Innymi słowy, by spełniały zasadę F.I.R.S.T. Piąty element tej zasady jest związany z pokryciem. Jednak zaczęliśmy używać JUnita do tworzenia testów integracyjnych i integracyjno-jednostkowych. W efekcie mechanizm związany z @RunWith stał się zaczepem dla pluginów. Coś, co miało pomagać w migracji z wersji 3.x, służy nam do zupełnie czegoś innego.

W dodatku mamy do czynienia z klasycznym dependency hell, gdy potrzebujemy wykorzystać kilka runnerów. Który będzie pierwszy? Czy kolejność jest prawidłowa? Co będzie, jak zamienię kolejnością runnery? Słabo.

Kolejna rzecz to zarządzanie zależnościami pomiędzy testami. JUnit tego nie ma. Czasami jednak jest to przydatna funkcjonalność. Nie tylko na poziomie testów integracyjnych, ale też po to by testy jednostkowe można było wykonywać według zasady „fail fast”. Co prawda można na poziomie np. mavena ustawić odpowiednią regułę, ale dotyczy ona wszystkich testów. Przydałaby się granulacja na poziomie klasy albo przynajmniej pojedynczego zestawu.

Paskudnie rozwiązane zarządzanie zależnościami. W sumie nie ma się co dziwić, bo prawdziwy test jednostkowy nie ma zależności. Tyle tylko, że czasami warto by jakąś fabryczkę do mocków zrobić, albo dorzucić jakiś niewielki helper… a tu dupa i trzeba to ręcznie wiązać, albo za pomocą runnerów.

Ostatnia rzecz to asercje. Pozostało po starszych wersjach i nie idzie tego zmienić. Z drugiej strony wystarczy podpiąć AssertJ i olać asercje JUnita.

Dlatego lubię TestNG

TestNG jest znacznie młodsze. Wersja 1.0 jest z 2004 roku, ale najstarsza na githubie wersja 5.13 z 2010. Najnowsza 6.11 z końca lutego 2017 🙂 Mamy tutaj wszystko to, co w JUnit jest upierdliwe, uciążliwe, albo czego nie ma, a by się przydało. Po pierwsze zależności pomiędzy testami możemy skonfigurować na poziomie samych testów. Zatem można przygotować zestaw testowy tak, by wywalenie się pojedynczego testu skutkowało nieuruchomieniem tylko części testów. To z kolei oznacza, że można tworzyć testy integracyjne we w miarę łatwy sposób. Co więcej, można budować całe drzewa testów. Wystarczy wykorzystać grupy, które można zagnieżdżać. Do tego dochodzi całkiem dobra granulacja Before/After i już mamy w pełni elastyczne narzędzie.

Po drugie TestNG posiada na pokładzie mechanizm modułów. Chcesz zrobić sobie takie małe DI, by postawić część kontekstu? Proszę bardzo, jeżeli tylko trzymasz się JSR-330, czyli Dependency Injection for Java. Jako implementacja wykorzystywany jest Google Guice, co daje całkiem wygodne API. Swoją drogą im częściej porównuję Springa i Guice tym bardziej lubię Guice.

Po trzecie may też do dyspozycji mechanizm DataProvider, który pozwala nam na szybkie tworzenie np. testów parametryzowanych. Coś, co w JUnit trzeba ogarnąć runnerem, tu działa out of box.

Rzeczy dobre w JUnit

Ciekawie rozwiązano problem weryfikacji wyrzucanych wyjątków. Możemy użyć do tego odpowiedniego parametru w adnotacji, ale to nie jest najlepsze podejście. Co prawda wychwycimy wyjątek, ale nie zawsze możemy powiedzieć, że to co złapaliśmy, jest tym, czego się spodziewamy. Dlaczego? Ponieważ czasami taki sam wyjątek może być wyrzucony w różnych punktach testu. Zamiast tak prymitywnego podejścia, dostępnego też w TestNG, możemy użyć adnotacji @Rule. Z jej pomocą zdefiniujemy odpowiednią regułę, która pozwoli nam na ciche przechwycenie wyjątku oraz jego weryfikację. TestNG posiada podobnie działający mechanizm.

Podejście JUnit5

Jak widać, jest trochę rzeczy złych w JUnit 4. Twórcy postanowili naprawić większość z nich. W tym celu całkowicie zmienili architekturę. Podzielili ją na trzy główne części.

Zanim przejdę do omówienia elementów, drobna uwaga. JUnit 5 jest obecnie w epoce milestonea łupanego. Opis dotyczy tego jak to wygląda na chwilę obecną. Xapewne nie będziemy mieli już wielu zmian, to jednak może okazać się, że gdy czytasz ten tekst jest on w jakimś stopniu nieaktualny.

JUnit Platform

Jest to główna „paczka” nowej biblioteki. Zawiera w sobie najważniejsze elementy. Po pierwsze Launcher, który jest odpowiedzialny za udostępnienie API platformy różnego rodzaju klientom. Dodatkowo wykonuje pracę związaną z wyszukaniem testów w classpath. Klientami są implementacje TestEngine. Sam TestEngine udostępniony przez platformę służy do filtrowania i uruchamiania testów w zależności od potrzeb. Chcesz mieć FitNess na pokładzie i uruchamiać testy pisane za pomocą tej biblioteki? Wystarczy dodać odpowiedni silniczek.

Same silniki testowe są wyszukiwane za pomocą mechanizmu SPI, czyli nie musimy nic dodatkowo konfigurować, by całość śmigała. Wystarczy, by nasza paczka spełniała założenia SPI. Kolejnym elementem jest commons, który zawiera bebechy i narzędzia. Używać na własną odpowiedzialność.

Do tego mamy jeszcze mały programik konsolowy do uruchamiania testów, pluginy do gradle i surefire oraz runner, pozwalający na uruchomienie testów JUnit 5 w środowisku JUnit 4. Ten ostatni element ma ułatwić migrację.

JUnit Jupiter

Jupiter to pierwszy z silników testowych dostarczonych przez bibliotekę. Służy do uruchamiania testów oraz ma API do tworzenia rozszerzeń. Rozszerzenia pozwalają na modyfikowanie środowiska testowego, dodawania pewnych elementów do testów itp. Pełnią podobną rolę co runnery JUnit4, ale robią to w zupełnie inny sposób.

JUnit Vintage

Drugi z silników testowych, służy do uruchamiania testów napisanych w JUnit 4 i Junit 3.

Konfiguracja

Konfiguracja JUnit 5 w mavenie jest śmiesznie prosta:

Listing 1. Konfiguracja w pom.xml


    
        
            maven-surefire-plugin
            2.19
            
                
                    org.junit.platform
                    junit-platform-surefire-provider
                    1.0.0-M3
                
                
                    org.junit.jupiter
                    junit-jupiter-engine
                    5.0.0-M3
                
                
                    org.junit.vintage
                    junit-vintage-engine
                    4.12.0-M3
                
            
        
    



    
        org.junit.jupiter
        junit-jupiter-api
        5.0.0-M3
        test
    
    
        junit
        junit
        4.12
        test
    

    
        pl.pragmatists
        JUnitParams
        1.0.6
        test
    

Przy czym jedna uwaga. Jak już chcemy korzystać z JUnit 5, to darujmy sobie próby konfiguracji JUnit 4 jako osobnego zadania dla surefire. Lepiej jest dodać silnik Vintage, który pod spodem po prostu odpala JUnit 4 niż kombinować z konfiguracją. Szkoda czasu, a i wynik będzie popieprzony. Dlaczego? Ponieważ JUnit 5 wykryje testy JUnit 4, ale bez silnika vintage ich nie uruchomi. Zatem w logu dostaniemy:

Listing 2. Uruchomienie testów JUnit 4 bez silnika Vintage

mar 28, 2017 11:38:09 AM org.junit.platform.launcher.core.ServiceLoaderTestEngineRegistry loadTestEngines
INFO: Discovered TestEngines with IDs: [junit-jupiter]
Running pl.koziolekweb.blog.fizzbuzz.FizzBuzzJUnit4IgnoreTest
Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.001 sec - in pl.koziolekweb.blog.fizzbuzz.FizzBuzzJUnit4IgnoreTest
Running pl.koziolekweb.blog.fizzbuzz.FizzBuzzJUnit4WithoutRunnersTest
Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0 sec - in pl.koziolekweb.blog.fizzbuzz.FizzBuzzJUnit4WithoutRunnersTest
Running pl.koziolekweb.blog.fizzbuzz.FizzBuzzJUnit4WithRunnersTest
Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0 sec - in pl.koziolekweb.blog.fizzbuzz.FizzBuzzJUnit4WithRunnersTest

Klasa jest, testu nie ma, a teraz pomyślcie, jak wygląda raport. Będzie zawierać wiele zer… to nie dobrze?

Podsumowanie

Czas na krótkie podsumowanie. JUnit 5 wygląda na bibliotekę, która pozwoli nam na lepsze zarządzanie testami. Same testy będą pisane trochę inaczej i istnieje szansa, że będą lepsze. Moim zdaniem warto już dziś przyjrzeć się temu projektowi i spróbować go wdrożyć we własnych projektach. Całość jest mniej więcej stabilna, a zatem nie będzie jakiś dziwnych wybuchów. W kolejnych artykułach przybliżę różnice pomiędzy JUnit 5 i 4, w zakresie tworzenia testów.

Monolit kontra mikroserwisy – obrazkowo

Monolit

Nurgle by baklaher

Mikroserwisy

Zstąpienie Aniołów – Krucjata inna niż wszystkie

Okładka Zstąpienie Aniołów

Tytuł: Zstąpienie Aniołów
Autor: Mitchel Scanlon
Rok: 2016
ISBN: 978-8-36165-644-9

Najtrudniejsza książka z serii Herezji Horusa za mną 😉 Jest inna. Po prostu. Gdy kilka lat temu czytałem recenzje Zstąpienia Aniołów, to przewijał się motyw nudy. Wersję angielską przeczytałem gdzieś 10 lat temu i jakoś nie pokochałem. Ostatnio wróciłem do tematu i przeczytałem polskie tłumaczenie. Książka jest dobra. Tyle tylko, że po pierwszych pięciu tomach HH można mieć trochę inne oczekiwania. W efekcie wielu czytelników dochodzi do wniosku, że całość wieje nudą. Jednak po kolei.

Akcja książki rozpoczyna się na kilka lat przed przybyciem Imperatora na Caliban. Lion el’Jonson nie jest jeszcze mistrzem Zakonu, ale już od początku wiemy, że nominacja to kwestia czasu. Historia opowiedziana jest z perspektywy Zahariela, jednego z zakonnych suplikantów, a później młodego rycerza. Mamy więc tutaj książkę raczej przygodową z elementami fantasy, ale powiedzmy sobie szczerze, to fantasy jest na poziomie bajki o św. Jerzym i smoku. Walczymy z dużym potworem i jest OK. Bliżej temu do „Zabójcy Trolli” niż do HH. Całość podzielona jest na cztery części i dwie pierwsze dotyczą właśnie okresu rycerskiego. Dwie kolejne to już opowieść z perspektywy Zahariela – astares.

Autor pomija tu proces tworzenia kosmicznego marine. Jedynie opisuje wstępną fazę selekcji związana z wyborem najtwardszych i najwytrzymalszych rycerzy i suplikantów. Pomija też kwestię przemiany starszych rycerzy, później jedynie wspomniane jest, że zostali poddani kuracji chemicznej, ale nie otrzymali geno-ziarna. W końcu akcja książki przenosi się na Sarosh, jedną z planet odkrytych w czasie krucjaty. Dalej standard – lokalsi okazali się wyznawcami jakiejś tam formy chaosu, zdradzili siły imperium i próbowali zabić Jonsona. Ten się wkurwił i…

No właśnie i zarządził uderzenie punktowe z wykorzystaniem niewielkich sił. To, co odróżnia Zstąpienie od innych książek z serii HH, to brak wojny totalnej. Nie znajdziemy tu opisów epickich bitw, poza jedną – zdobyciem twierdzy rycerzy Lupusa w czasach przed imperialnych, ale i tu raczej są to Krzyżacy. Nie ma tytanicznych zmagań prowadzonych przez tysiące astares z użyciem setek czołgów, samolotów i wsparciem Collegia Titanica. Nie ma tego wszystkiego, co jest znakiem charakterystycznym HH. W zamian jest opowieść o wewnętrznej rywalizacji w zakonie. Każdy, kto zna choć trochę świat w40k, odnajdzie tu wiele informacji i wyjaśnień dotyczących, takiego, a nie innego losu Calibanu.

Książka jest dobra. Jest dobrym czytadłem. Jednak zanim zacznie się lekturę, należy odrzucić schematy znane z HH. Wtedy mamy do czynienia z całkiem dobrą książką. Na koniec doczepię się do jednej rzeczy. Dialogi w książce są takie jakieś koślawe. Nie są drętwe, ale nie ma w nich tego czegoś. Większość, to pierdolenie o Chopinie, autor starał się chyba stylizować je na „teksty prawdziwych rycerzy”, ale mu nie wyszło. I jest to jedyny poważny minus.

Iterator filtrujący, czyli kontrakt iteratora w dwóch odsłonach

Dziś o tym, że kontrakt zawarty w dokumentacji można interpretować na wiele sposobów oraz dlaczego SRP jest istotne.

Na 4p padło pytanie, jak ogarnąć problem iteratora, który będzie przy okazji filtrował kolekcję. Sama implementacja jest stosunkowo prosta:

Listing 1. Iterator filrtujący

public class FilteringIterator<T> implements Iterator<Optional<T>> {

	private final Iterator<T> iter;

	private final Predicate<T> predicate;

	public FilteringIterator(Collection<T> collection, Predicate<T> predicate) {
		this.iter = collection.iterator();
		this.predicate = predicate;
	}

	@Override
	public boolean hasNext() {
		return iter.hasNext();
	}

	@Override
	public Optional<T> next() {
		T next = iter.next();
		return Optional.ofNullable(next).filter(predicate);
	}
}

Założenie jest następujące. Idziemy przez kolekcję i delegujemy zachowania iteratora do iteratora powiązanego z tą kolekcją. Przy czym metoda next pełni role adaptera, który zwraca Optional[T] zamiast T. jarekr000000, zwrócił mi jednak uwagę, że należało by logikę predykatu przenieść do metody hasNext. Tyle tylko, że w takim wypadku otrzymamy iterator, który będzie iteratorem until (limitującym). To znaczy, będzie zwracać jakąś wartość, dopóki warunek jest spełniony.

Listing 2. Iterator limitujący

public class UntilIterator<T> implements Iterator<T> {

	private final Iterator<T> iter;

	private final Predicate<T> predicate;
	private Optional<T> current;


	public UntilIterator(Collection<T> collection, Predicate<T> predicate) {
		this.iter = collection.iterator();
		this.predicate = predicate;
	}

	@Override
	public boolean hasNext() {
		boolean b = iter.hasNext();
		if (b) {
			current = Optional.ofNullable(iter.next())
					.filter(predicate);
			return current
					.map($ -> Boolean.TRUE) // element istnieje i spełnia warunek
					.orElse(false); // element nieistnieje albo niespełnia waruneku
		}
		return false;
	}

	@Override
	public T next() {
		return current.orElseThrow(NoSuchElementException::new);
	}
}

Implementacja jest oczywiście po łebkach i nie zapewnia prawidłowego działania w środowisku współbieżnym.

Na czym polega różnica?

Na początek rzućmy okiem na kontrakt metody hasNext:

Returns true if the iteration has more elements. (In other words, returns true if next() would return an element rather than throwing an exception.)

źródło

Pierwszy z iteratorów będzie zwracał coś, Optional albo Empty, dla każdego elementu w oryginalnej kolekcji. Oznacza to też, że metoda hasNext będzie zwracać true dopóki w oryginalnej kolekcji są jeszcze elementy. Oznacza to też, że jeżeli element nie spełnia warunków testu, to i tak metoda next nie powinna rzucać wyjątkiem.

Drugi z iteratrów zachowa się zupełnie inaczej. W tym przypadku, hasNext zwróci false dla pierwszego elementu niespełniającego warunku. Stanie się tak nawet wtedy, gdy oryginalna kolekcja będzie miała jeszcze elementy. Tym samym metoda next może rzucić wyjątek, nawet wtedy, gdy kolekcja nadal posiada elementy spełniające warunek.

Kolejna różnica, to wartość zwracana z metody next. Pierwszy z iteratorów opakuje ją w Optional. Drugi zwróci wartość taką jakiej oczekujemy iterując przez kolekcję obiektów o danym typie. Czy ta różnica ma znaczenie? Czasami ma, ponieważ dla pierwszego iteratora musimy wykonać operację „rozpakowania” wartości. W przypadku drugiego już nie. Dobrze obrazują to testy:

Listing 3. Test dla iteratora filtrujacego

public class FilteringIteratorTest {

	@Rule
	public ErrorCollector collector;

	private List<Integer> ints;

	@Before
	public void setUp() throws Exception {
		collector = new ErrorCollector();
		ints = asList(4, 3, 2, 1);
	}

	@Test
	public void filterOutSomeInts() throws Exception {
		FilteringIterator<Integer> it = new FilteringIterator<>(ints, i -> i % 2 == 0);

		// 4
		assertThat(it.hasNext()).isTrue();
		assertThat(it.next()).isPresent().contains(4);

		// 3
		assertThat(it.hasNext()).isTrue();
		assertThat(it.next()).isEmpty();

		// 2
		assertThat(it.hasNext()).isTrue();
		assertThat(it.next()).isPresent().contains(2);

		// 1
		assertThat(it.hasNext()).isTrue();
		assertThat(it.next()).isEmpty();

		// no more elements
		assertThat(it.hasNext()).isFalse();

		catchException(it).next();
		collector.checkThat(caughtException(), instanceOf(NoSuchElementException.class));

	}
}

Listing 4. Test dla iteratora limitującego

public class UntilIteratorTest {

	@Rule
	public ErrorCollector collector;

	private List<Integer> ints;

	@Before
	public void setUp() throws Exception {
		collector = new ErrorCollector();
		ints = asList(4, 3, 2, 1);
	}

	@Test
	public void filterUntilSomeInts() throws Exception {
		UntilIterator<Integer> it = new UntilIterator<>(ints, i -> i % 2 == 0);
		// 4
		Assert.assertTrue(it.hasNext());
		Assert.assertEquals(it.next().intValue(), 4);

		// 3 → false
		Assert.assertFalse(it.hasNext());
		catchException(it).next();
		collector.checkThat(caughtException(), instanceOf(NoSuchElementException.class));
	}

}

Wszystko ładnie pięknie, ale jest jeden problem.

SRP

Zasada pojedynczej odpowiedzialności jest całkiem fajna. Tyle tylko, że tu jest złamana. Jak? Nasze iteratory robią dwie rzeczy naraz. Po pierwsze poruszają się po kolekcji. Po drugie manipulują elementami kolekcji. W efekcie już na samym początku mamy problem. Autor pytania łamiąc SRP, wpędził się w kłopoty. Jarek i ja dyskutowaliśmy nad problemem, który w praktyce nie powinien istnieć.

Pozostaje jeszcze kwestia kontraktu metody hasNext. Jest on na tyle ogólny, że oba iteratory mogą w na swój sposób określić, jak go realizują. Ze szczególnym uwzględnieniem co oznacza „brak kolejnych elementów”.

Zachłannie, leniwie, współbieżnie – czego nauczyłem się od Jose Valima

Warto jeździć na konferencje i słuchać mądrzejszych do siebie. Warto, ponieważ często nie dowiemy się nic nowego, ale sposób przekazania wiedzy pozwoli na uporządkowanie kilku rzeczy.

Jose na tegorocznych LambdaDays opowiadał o GenStage i Flow. Jednak nie to jest najważniejsze. Jakby przy okazji omówił trzy różne podejścia do pracy z danymi.

Podejście zachłanne

Jest najprostsze. Ładujemy wszystko do pamięci i jazda. W takim przypadku program nie zawiera żadnej dodatkowej magii:

Listing 1. Zachłanne przetwarzanie danych.

def eager() do
  File.read!("plwiki-20170301-all-titles")
    |> String.split("\n")
       |> Enum.flat_map(&String.split/1)
       |> Enum.reduce(%{}, fn word, map ->
                                Map.update(map, word, 1, & &1 + 1)
                           end
                     )
end

W powyższym programie wczytujemy plik tekstowy i chcemy policzyć wystąpienia poszczególnych słów. Nie ma tu nic skomplikowanego. Funkcja read! wczytuje cały plik. Następnie dzielimy go na linie, a z linii budujemy listy (enumerable) słów za pomocą flat_map. Na koniec wszystko redukujemy. Takie podejście ma jednak pewną wadę. Musimy zapewnić odpowiednio dużo pamięci dla naszego programu. Oczywistym rozwiązaniem jest…

Podejście leniwe

W tym przypadku ustalamy, że będziemy czytać plik linia po linii i przetwarzać na raz całą linię.

Listing 2. Leniwe przetwarzanie danych.

def lazy() do
  File.stream!("plwiki-20170301-all-titles", [], :line)
    |> Stream.flat_map(&String.split/1)
    |> Enum.reduce(%{}, fn word, map ->
                            Map.update(map, word, 1, & &1 + 1)
                        end
                  )
end

Funkcja stream! jako trzeci parametr przyjmuje :line. Dzięki czemu odczyta na raz tylko jedną linię z pliku. Udało nam się wyeliminować dużą konsumpcję pamięci, ale pytanie, co jak chcemy przyspieszyć?

Podejście współbieżne

Jeżeli dane są wzajemnie niezależne, to możemy spróbować usprawnić proces ich przetwarzania. Wystarczy, by przetwarzanie odbywało się na wielu procesorach.

Listing 3. Równoległe przetwarzanie danych.

def parallel() do
  File.stream!("plwiki-20170301-all-titles")
    |> Flow.from_enumerable()
    |> Flow.flat_map(&String.split/1)
    |> Flow.partition()
    |> Flow.reduce(fn -> %{} end, fn word, map ->
                                     Map.update(map, word, 1, & &1 + 1)
                                  end
                    )
    |> Enum.into(%{})
end

Narzędziem, które nam to ułatwia, jest Flow. Dzięki niemu możemy zaimplementować zarówno mechanizm równoległego przetwarzania danych, z wykorzystaniem GenServer, jak i wykorzystać tzw. Back-pressure, by dostroić obciążenie komponentu odczytującego dane do możliwości systemu.

Jednak w tym przypadku spotkamy jeszcze jeden problem. Jeżeli redukcja ma być nieblokująca, to musimy jakoś pogodzić pisanie do mapy przez wiele wątków. Chcemy uniknąć sytuacji, gdy dwa wątki modyfikują wartość pod tym samym kluczem. W tym celu Flow udostępnia funkcję partition, która gwarantuje, że dany klucz będzie „przywiązany” do wątku. Na koniec wystarczy zsumować mapy. Jest to bardzo proste, ponieważ wiemy, że zawierają unikalne klucze.

Podsumowanie

Ok, ale co w tym dziwnego? Zauważcie, że do naszego problemu podeszliśmy tak, by na początek zaaplikować rozwiązanie najprostsze. Następnie komplikowaliśmy rozwiązanie, jednocześnie uzyskując nowe możliwości i lepsze rezultaty. Warto tak działać, ponieważ w ten sposób minimalizujemy szansę na popełnienie błędu. Niestety zauważyłem, że coraz więcej osób chce od razu mieć rozwiązanie, które jest skomplikowane. Jednak produkt końcowy zawiera dużo dziwnych błędów.

Na koniec małe wizualne porównanie działania poszczególnych rozwiązań. Na dole zrzut z htopa. Erlang śmiga na rdzenie od 1 do 4. Na rdzeniach 7 i 8 śmiga nagrywanie pulpitu. Obserwujcie, jak dużo pamięci jest zajęte. Całość odpalać w wysokich detalach na pełnym ekranie.

Pliki w tym tekście to dumpy wikipedii, zanim jednak pobierzesz jakiś plik weź pod uwagę możliwości swojego komputera 😉

Kod znajdziecie tu.