Posiadanie w projekcie testów architektury (architecture tests) powoli staje się czymś oczekiwanym. Użytkownicy Kotlina od niedawna mają problem bogactwa wyboru. Mogą użyć biblioteki ArchUnit, już ustabilizowanej, całkiem szeroko znanej, stworzonej dla Javy, ale działającej też dla Kotlina. Pojawiła się jednak alternatywa. Konsist to narzędzie napisane specjalnie pod Kotlina, o krótszej historii, ale zdobywające popularność.
W dalszej części artykułu porównam ze sobą te biblioteki. Nie będę kopiował haseł reklamowych: zawodowo używam obu od ponad roku, więc mam sporo obserwacji natury praktycznej.
Po lekturze będziesz (mam nadzieję) wiedzieć, czy do twojej sytuacji bardziej będzie pasować ArchUnit, czy też Konsist, a może potrzebujesz obu.
Moim celem jest porównanie, więc nie omówię wszystkich cech obu narzędzi i będę się streszczał. Spragnionym szczegółów na temat konkretnej biblioteki polecam moje wcześniejsze artykuły: wprowadzenie do ArchUnita, wprowadzenie do Konsista.
Artykuł jest skierowany do programistów Kotlina. Jeśli programujesz tylko w Javie, to z Konsista nie skorzystasz, bo obsługuje tylko język od JetBrains. Mimo wszystko, polecam pod uwagę wyliczenie niedostatków ArchUnita – nie podaję tu lepszego narzędzia do Javy, ale moim zdaniem lepiej znać wady swojego narzędzia, niż ich nie znać.
Spis treści
Elevator pitch
Oba narzędzia są bibliotekami open source, na przyjaznej komercyjnie licencji Apache, hostowanymi na GitHubie.
Starszy ArchUnit ma w tym momencie 3300 gwiazdek.
Młodszy Konsist ma 1500 gwiazdek.
Umożliwiają pisanie testów architektury, czyli nakładania wymagań na to, jak układamy klasy, metody, pakiety, adnotacje w naszym kodzie (też kodzie testów). Lub, patrząc inaczej, są „linterami rozumiejącymi strukturę kodu” – a przynajmniej Konsist pozycjonuje się właśnie za pomocą neologizmu (w sumie bardzo trafnego) structural linter.
Porównywane aspekty
Zdrowie biblioteki
Zacznijmy od rozważenia, czy włączamy do swojego projektu narzędzie, które będzie żyć, czy też umrze po niedługim czasie.
ArchUnit wypuścił pierwszą stabilną wersję pod koniec roku 2022. Wydania wypuszcza jeden człowiek, ale są w miarę regularne. Nad zgłaszanymi problemami jest podejmowana dyskusja. Najnowsza wersja to 1.4.0, co oznacza, że po ustabilizowaniu API, nie nastąpiły breaking changes i możemy mieć nadzieję, że nie będziemy musieli co kwartał poprawiać w testach architektury błędów kompilacji w stu miejscach. Czasem zdarza się, że wychodzą poprawki do starych wersji, na przykład po wersji 1.4.0 pojawiła się jeszcze 1.3.1, która dodawała obsługę Javy 24.
Konsist wydał pierwszą wersję, czyli 0.7.7, na początku roku 2023. Minęło 1,5 roku i obecna wersja to 0.17.3, czyli ciągle niestabilna. Nad projektem pracuje więcej niż jedna osoba, jest publikowany roadmap, ale nie wiadomo, ile jeszcze potrwa stabilizowanie API. Trzeba się liczyć z tym, że będziemy musieli aktualizując wersję poprawiać to i owo w naszym kodzie, który przestanie się kompilować. Tempo wypuszczania wersji nie jest stabilne ani przewidywalne. Nie zdarzyło się jeszcze, żeby poprawki trafiały do starszych branchy.
Nie można też liczyć, że zgłaszane przez nas problemy będą szybko poprawiane. Twórcy Konsista muszą rozłożyć siły między szybkie doprowadzanie API do stabilności i poprawki błędów. Póki co wybierają to pierwsze. W konsekwencji na GitHubie jest wyłączona opcja zgłaszania błędów. Można otwierać „dyskusje”, ale nie ma gwarancji, że ktoś na nie odpowie. Przykładowo, 5 miesiący temu zgłaszałem, że Konsist ma problem z Windowsem ze względu na brak normalizacji slashy – nikt jeszcze nie odpisał, czy mam rację, czy jej nie mam, a jeśli mam, to co dalej.
Kompatybilność językowa
ArchUnit jest napisany w Javie, powstał w Javie, ale operuje na bytecodzie JVM, czyli nie jest ważne, w czym piszemy nasze źródła: może być to Kotlin, może być to Java, albo Scala jeśli ktoś woli. Testów ArchUnita to nie obchodzi, logika jest jedna i ta sama.
Konsist jest napisany w Kotlinie, jest rozwijany przez ludzi tworzących na aplikacje dla Androida. Jeśli robisz w Kotlinie backend, to Konsist będzie dobrze działać, ale jeśli robisz aplikacje mobilne, to w bonusie masz rzeczy specjalnie dla ciebie.
Uniwersalność ArchUnita ma złą stronę: ciężko wyrazić w jego API reguły obejmujące składnię specyficzną dla Kotlina, a często jest to wprost niemożliwe (np. nie da się sprawdzić, czy deklaracja ma modyfikator internal
).
Konsist rozumie Kotlina ze wszystkimi szczegółami składni. Tyle że kod do analizy wczytuje przez wbudowany kompilator Kotlina. Czyli klas Javy (albo Scali itp.) po prostu w ogóle nie widzi. Jeśli w projekcie mieszasz Javę i Kotlina, to Konsist nie będzie najlepszym wyborem. Albo dokładniej: nie powinien być jedynym wyborem.
Ponieważ nic nie stoi na przeszkodzie, żeby mieć obok siebie testy pisane w Konsiście i w ArchUnicie. Więcej o tym pod koniec.
Biblioteki starają się zapewnić szeroką zgodność z wersjami języka. ArchUnit ma czytać bytecode od wersji definiowanej przez Javę 8 do wersji 24. Dla Konsista minimalne wersja to Java 8 i Kotlin 1.8.
Kompatybilność z bibliotekami
Zarówno ArchUnit jak i Konsist starają się unikać uwiązania do jednego konkretnego „stacka” technologicznego. Dostajemy od twórców dobrze działającą integrację z JUnitem 5, ale inna biblioteka do testów też powinna działać.
Dokumentacja
ArchUnit ma porządną dokumentacją, wprowadzającą krok po kroku różne elementy biblioteki. Są pewne braki, tekst nie wchodzi bardzo głęboko w szczegóły, nie oferuje kompletnych poradników na każdą okazję typu „jeśli chcesz A, to napisz B”, ale główna funkcjonalność jest opisana jasno i z potrzebnymi szczegółami.
Dokumentacja Konsista wygląda nowocześniej, ale jest jeszcze niekompletna. Już teraz można tam jednak znaleźć dużo pomocnych informacji, jak zacząć pracę z biblioteką. Świetną rzeczą jest sekcja „Inspiration > Snippets”, gdzie są wymienione różne przykłady problemów i odpowiadający każdemu kod Konsista.
Konflikty zależności
Zarówno ArchUnit jak i Konsist przynoszą ze sobą tranzytywne zależności, które powodują, że izoluję je od pozostałych testów.
Problemem nie jest rozmiar zależności w megabajtach, ale ich rola. Obie biblioteki lubią wybiegać zbytnio do przodu w podnoszeniu wersji kluczowych tranzytywnych zależności (podczas gdy biblioteki powinny trzymać swoje zależności tak niskie, jak się da – ale to temat na inny artykuł). Na przykład ArchUnit przeskoczył na SLF4J 2 bardzo szybko, mój Spring Boot miał SLF4J 1, po połączeniu wszystkiego razem logi w ogóle się nie drukowały. Z kolei Konsist podniósł Kotlina z 1.9 na 2.0 kilka dni po premierze, a mnie niezbyt uśmiechało się kompilowanie kodu Kotlinem 1.9, żeby potem uruchamiać testy na runtimie Kotlina 2.0.
Ogólnie, każda z bibliotek ma duży negatywny potencjał do destabilizacji działania waszych nie-architektonicznych testów, zwłaszcza testów działających na pełnym stacku, z bazą danych i serializacją JSON-a (a takie właśnie testy najbardziej warto pisać). Dlatego lepiej nie dać im takiej szansy i izolować. W Gradle’u służy do tego osobny source set.
src/
main/
test/
architectureTest/
Instrukcja tworzenia jest na przykład w dokumentacji „Isolate Konsist Tests”.
Łatwość pisania
Zanim dojdziemy do pisania, zacznijmy od łatwości czytania. W obu bibliotekach da się tworzyć bardzo czytelne, deklaratywne testy:
noMethods().that() .areDeclaredInClassesThat( simpleNameContaining("Test") ) .and().haveRawReturnType("void") .should().bePublic() .andShould().notBeAnnotatedWith("org.junit.jupiter.api.Test")
Powyższy test ArchUnita nie było jednak łatwo napisać. Problemem jest znalezienie odpowiedniej metody z API. Gdy mamy ciąg wywołań (jak kropka po .that()
), to pół biedy, bo IDE zna typ i podsunie metody do wywołania. Gorzej, gdy trzeba użyć predykatu (simpleNameContaining
). ArchUnit ma mnóstwo wbudowanych predykatów, ale nie jest łatwo je znaleźć, bo są rozproszone po wielu plikach. Ten tutaj to na przykład com.tngtech.archunit.core.domain.JavaClass.Predicates.simpleNameContaining
. Są też predykaty dla metod (JavaMethod.Predicates
), dla uogólnień (JavaCodeUnit.Predicates
). Są też statyczne factory methods o nazwach typu notBeAnnotatedWith
. Biada jednak temu, kto je zaimportuje. Bo zwracają nie DescribedPredicate
(do użycia po that()
), lecz ArchCondition
(do użycia po should()
). ArchUnita używam dłużej niż Konsita, chyba jakieś 4 lata, ale pisanie nie wychodzi mi płynnie – prędzej czy później pojawia się konieczność zrobienia static importu i rzadko udaje mi się dobry trafić. Czasem poddaję się przy szukaniu i piszę klasę anonimową, bo wychodzi szybciej. Co też nie jest wygodną i zwięzłą opcją.
O ile ArchUnit to taki DSL na pisanie „query” w kodzie, to Konsist to taki trochę stream processing.
konsistScope.functions() .filter { it.isExtension } .assertTrue { it.hasPrivateModifier || it.hasInternalModifier }
W tym kodzie functions
zwraca kotlin.collections.List<KoFunctionDeclaration>
, więc .filter { }
to zawołanie z biblioteki standardowej Kotlina. Musimy zapamiętać, żeby użyć .assertTrue { }
dostarczanego przez Konsista (to extension method), ale poza tym programuje się naturalnie i z lepszą pomocą ze strony IDE.
Czas wykonania
O ile Konsist ułatwiał pisanie jakiegokolwiek testu, to już nie ułatwia pisania szybkiego testu.
Programiści nierozumiejący, jak działa Kotlin, będą powodowali szkody kodem typu:
functions() .filter { foo } .filter { bar } .filter { baz }
Powyższy kod alokuje 4 bardzo duże listy, bo w Kotlinie filter
na liście jest eager, czego często nie zauważają programiści przychodzący z Javy.
Oczywiście, znając Kotlina, łatwo zamienić ten łańcuch wywołań na coś wydajniejszego. Tyle że nawet poprawnie napisany test Konsista nie powala na nogi szybkością wykonania. Jedna metoda testowa może w większym repozytorium wykonywać się paręnaście sekund.
ArchUnit zachowuje się inaczej: czas wykonania nie zależy zbytnio od sposobu pisania testu. Większa część tego czasu schodzi na wstępne załadowanie klas, ale potem wykonanie testów jest naprawdę szybkie, zazwyczaj poniżej sekundy.
Czyli jeśli planujemy pisać bardzo dużo testów architektury w dużym repozytorium, to czas wykonania Konsista będzie ciągle rosnąć, za to ArchUnita nie będzie się znacząco zwiększać.

Zakazanie wołania pewnych metod
W ArchUnicie da się zapisać reguły typu:
noClasses().that(are(kotlinClass)) .should().callMethod(Optional::class.java, "orElse", Object::class.java)
(przykład z artykułu ArchUnit – praktyczne zastosowanie, tam wyjaśniam, skąd taka reguła)
W Konsiście można co najwyżej zakazać importu danej klasy. Co, po pierwsze, nie wyłapie wszystkiego (klasy da się użyć też bez importowania jej). Po drugie, nie pozwala na granularność: ustalenie, że klasy wolno używać, ale z wszystkich metod tylko wybranej jednej czy dwóch metod nie wolno wołać.
Zakazanie pewnych importów
Czasem nie przeszkadza nam wołanie pewnej metody (na przykład Optional.of()
), ale przeszkadza nam już pewien zapis (typu return of("success")
). Są metody, których nie powinno się używać w stylu importu statycznego (w Kotlinie nie używa się słowa kluczowego static
w importach, ale da się zrobić import, który będzie działał tak samo, jak statyczny import w Javie, więc nazwę oba przypadki „importem w stylu importu statycznego”).
Takiej reguły nie da się zrobić w ArchUnicie, który zajmuje się bytecode’em i importów nie widzi.
Bardzo proste jest za to napisanie takiego zakazu w Konsiście, gdzie mamy jako „first-class citizen” importy w skanowanym projekcie czy repozytorium.
Projekty wielomodułowe
Wyobraźmy sobie taką strukturę:
module1/
src/
main/
test/
…
module20/
src/
main/
test/
app/
src/
main/
test/
W ArchUnicie nie jest łatwo pokryć wszystko testami architektury. Jeśli uruchomimy test ArchUnita z app/src/test, to owszem, będzie miał wczytane klasy z module1…module20. Tyle że wyłącznie klasy z main/
, bo tak działają zależności w Gradle’u i Mavenie. Opuszczenie skanowania katalogów test/
nie wchodzi w grę – jak pisałem, jest dużo reguł, które warto zastosować do testów.
Wyjścia są dwa:
- robimy skomplikowane i nienaturalne zmiany w konfiguracji buildsystemu, żeby zbiorczy moduł
app/
widział nie tylkomain/
, ale teżtest/
z zależności - lub 20 razy zakładamy testy architektury; pilnujemy też, żeby dla każdego nowego modułu autorzy pamiętali o założeniu tam testów architektury
Wyjście 2 nie jest wcale takie złe. Można wygodnie współdzielić logikę testów, tak żeby nie robić żadnego kopiuj-wklej między modułami. Gorzej z pilnowaniem, żeby każdy nowy moduł dostał integrację z ArchUnitem.
Problem nie istnieje w Konsiście – tam domyślnie biblioteka skanuje każdy plik Kotlina w repozytorium. Co prawda czasem skanuje za dużo (biada temu, kto ma kolegów w zespole, który swoje branche w Gicie nazywają z końcówką .kt
, bo wszystko to ląduje w <REPO>/.git
i potrafi zamieszać w Konsiście), ale w ogólnym rozrachunku jest łatwiej.
Zakładamy jeden test Konsista na całe repozytorium i nie potrzebujemy nic więcej.
Dziury w skanowaniu
Specyfiką ArchUnita jest, że nie można pozwolić mu skanować byle czego.
Nie chcemy, żeby nasze testy architektury robiły się czerwone, bo reguły, które wymyśliliśmy, nie są przestrzegane przez org.apache.commons:commons-lang3
. Dodatkowo, gdyby pozwolić ArchUnitowi na analizę wszystkiego, co jest tylko na classpath, to analiza zajęła by wieki, i to mieląc biblioteki open source zamiast kodu, który piszemy.
Typowo, jeśli test piszemy jako com.acme.shop.webapp.ArchTest
, to wewnątrz konfigurujemy skanowanie na com.acme.shop.webapp
.
Zdarzają się jednak programiści, którzy walcząc ze sposobem, w jaki testy Spring Boota szukają beanów, tworzą klasy w dziwnych miejscach. Zostając przy naszym com.acme.shop.webapp
: taki fan niespodzianek i niekonformistyczny wróg konwencji bez żenady założy klasę w src/test/mocks/MyBean.java
. W środku mogą być dowolne śmieci, a niestety nasze testy architektury ich nie wykryją.
Ciężko z takimi negatywnymi efektami nadmiernej inwencji walczyć z pomocą ArchUnita. To niestety dziura w koncepcji tego narzędzia.
Konsist nie ma takiego problemu. Jeśli każemy mu zeskanować cały projekt, wczyta wszystko, niezależnie od zadeklarowanego pakietu (lub braku takiej deklaracji). Warto zauważyć, że to kolejna przewaga Konsista: łatwo za jego pomocą zakazać korzystania z domyślnego (tzn. pustego) pakietu.
Konsist problem oddzielania kodu projektu od kodu zewnętrznych bibliotek rozwiązuje przez inny sposób działania: chodząc samodzielnie po systemie plików (jeśli coś jest plikiem, to musi być częścią projektu, a nie biblioteką). ArchUnit natomiast niczego sam nie wyszukuje; bazuje na tym, co dostał wczytane na classpath przez JVM uruchamiający testy.
Reguły tylko dla testów
Czasem chcemy w testach architektury traktować klasy testowe inaczej. Na przykład zakazywać pewnych złych praktyk w kodzie produkcyjnym, ale dać taryfę ulgową testom. Lub odwrotnie, pisać reguły, które mają sens w testach, ale dla kodu produkcyjnego nie mają sensu.
Konsist załatwia problem przejrzyście i elegancko: wołamy .filter { it.resideInSourceSet("test") }
. Konsist dla klas, plików itp. przechowuje informacje, w jakim katalogu zostały zadeklarowane.
W przypadku ArchUnita takiej informacji nie ma, co daje kilka opcji, ale żadnej pozbawionej wad.
Można iść na łatwiznę i próbować rozróżniać klasy po nazwie („jak coś ma Test
w nazwie, to jest testem”). Tyle że czasem zdarzają się klasy produkcyjne z „Test” w nazwie (słowo jak słowo), a w źródłach testowych są klasy pomocnicze (np. matchery), które słowa „Test” w nazwie nie posiadają.
Można kombinować ze skanowaniem (importOptions = [OnlyIncludeTests]
). Tyle że wtedy zamiast jednej klasy z regułami mamy dwie (nie-testy i testy) albo wręcz trzy (nie-testy, testy, wszystko). Im więcej klas z regułami, tym trudniej je utrzymać, płacimy też większy koszt na start (skanowanie ma duży czynnik stały, więc dzieląc klasę startującą w 10 sekund na dwie klasy skanujące po połowie, raczej dostaniemy dwie klasy po 7 sekund niż dwie klasy po 5 sekund).
Można sprawdzać, czy klasa używa testowych adnotacji np. JUnita. Lepsze niż poleganie na nazwie, ale bardziej skomplikowane a i tak ciężko napisać, żeby nic nie przegapić.
Pomijanie naruszania reguł
Biblioteki różnią się sposobem, w jaki możemy zignorować wybrane miejsca w kodzie, które naruszają reguły.
Konsist robi to w stylu linterów jak CheckStyle czy Ktlint, czyli dane miejsce w kodzie opatrujemy adnotacją @Suppress("konsist.my rule")
.
W przypadku ArchUnita trzeba założyć w resources/
plik o nazwie archunit_ignore_patterns.txt
, a w środku z użyciem wyrażeń regularnych dopasować miejsce w kodzie.
Co, gdy mamy do czynienia z dużym projektem o dłuższej historii i kiepskiej jakości kodu? Sposoby wspomniane powyżej się nie sprawdzą, nie będziemy ręcznie robić setek edycji w repozytorium.
ArchUnit pozwala załatwić sprawę hurtowo, przez zdefiniowane „baseline”, czyli poziomu kodu, od którego startujemy. Ma na tę funkcję swoją własną nazwę (zamrażanie reguł, czyli freezing) – narzędzie zapisuje tak jakby snapshot aktualnych miejsc, gdzie reguły są naruszone. Przy uruchamianiu testów architektury kod łamiący regułę nie jest raportowany, o ile jest zapisany w „snapshocie”. „Freeze store” to zwykły plik tekstowy trzymany w Gicie, a cały mechanizm jest całkiem przejrzysty i sprawny, choć plik trzeba aktualizować gdy zmienimy nazwę klas lub metod.
Konsist nie daje nic podobnego. Dokumentacja wspomina hasło baseline, ale tylko po to, żeby się tłumaczyć, że ta funkcja jest planowana na przyszłość. Można samemu zakodować wyjątki na fragmenty kodu, dodając filter { }
na pakiety, moduły itp., ale wymaga to pracy.
Tabelka z podsumowaniem
Kryterium | ArchUnit | Konsist |
---|---|---|
Ryzyko porzucenia | 🟡 niewielki zespół | 🟡 niewielki zespół |
Ryzyko breaking changes | 🟢 poza etapem 1.0, mało breaking changes w przeszłości | 🟡 długo nie ma 1.0, zdarzały się zmiany psujące kompilację |
Wiele branchy | 🟢 jeden rozwijany branch, ale czasem wychodzą patche na błędy do starszych | 🟡 nie ma jeszcze 1.0, więc nie ma poprawek wstecz |
Obsługiwane języki | 🟢 dowolny z JVM | 🟡 Kotlin |
Wsparcie dla Kotlina | 🟡 ograniczone, w zasadzie robimy reverse engineering z bytecode’u JVM | 🟢 pokrycie wszystkich aspektów składni |
Kompatybilność z bibliotekami | 🟢 | 🟢 |
Dokumentacja | 🟡 | 🟡 |
Czytelność pisanych testów | 🟢 | 🟢 |
Łatwość pisania reguł | 🔴 trudne do znalezienia importy statyczne | 🟢 |
Czas wykonania | 🟡 duży narzut na start, ale pojedyncze reguły wykonują się błyskawicznie | 🔴 czas szybko rośnie z dodawaniem reguł |
Zakaz wołania metod | 🟢 | 🔴 |
Zakaz importów | 🔴 | 🟢 |
Projekty wielomodułowe | 🟡 konieczność pewnego copy-paste | 🟢 można w 1 teście pokryć całe repozytorium |
Projekty ze złą organizacją pakietów | 🔴 łatwo przegapić skanowanie jakiegoś pakietu | 🟢 skanowane jest wszystko |
Rozróżnanie main/test | 🔴 | 🟢 |
Ignorowanie 1 miejsca w kodzie | 🟡 regex w osobnym pliku | 🟢 @Suppress |
Baseline | 🟢 automatyczne generowanie | 🔴 trzeba ręcznie odfiltrować wyjątki w kodzie reguły |
Zwycięzca porównania
Jeśli szukacie prostej odpowiedzi, kto jest zwycięzcą, a kto przegranym w tej rywalizacji – nie mogę jej dać. To są narzędzia, które w pewnym stopniu się pokrywają, ale są jednak narzędziami innego typu.
ArchUnit to bardziej narzędzie do static analysis z łatwym pisaniem wyrażeń.
Konsist to bardziej sprytny linter przeniesiony z poziomu linijek kodu na typy, funkcje i klasy.
Nie na darmo ArchUnit reklamuje się jako narzędzie do „architecture tests” i generowaniem UML-i, a Konsist pisze o sobie „structural linter”. Twórcy mają odmienne wizje, jaki jest charakter każdej biblioteki.
Łączenie ArchUnita z Konsistem
Jak widać z tabelki, są pewne aspekty, gdzie jedno z narzędzi błyszczy, a drugie kuleje, albo w ogóle nie obsługuje takiego przypadku użycia.
Dodanie do jednego projektu zarówno ArchUnita jak i Konsista nie powoduje konfliktów zależności między nimi (dobrze za to izolować je od normalnych testów – instrukcja w sekcji Konflikty zależności). Można więc użyć ich obu, żeby każde narzędzie zajmowało się tym, w czym jest dobre.
Na przykład ogólne reguły zrobić w ArchUnicie, żeby sprawdzały zarówno Javę i Kotlina, a brakujące szczegóły dotykające składni Kotlina dopisać w Konsiście.
Problemem ArchUnita są dziury w skanowaniu – jeśli ktoś utworzy klasę w nieprzewidywalnym pakiecie, narzędzie będzie na nią ślepe. Takie dziury można załatać Konsistem, który skontroluje, czy pliki są tylko w tych pakietach, które są skanowane przez ArchUnita (nie podaję kodu, ale wystarczą 2 linijki w teście Konsista, żeby refleksją wyciągnąć listę pakietów z adnotacji ArchUnita).
Obecnie w projektach, którymi zajmuję się zawodowo, każde repozytorium ma wydzielony source set architectureTest z jednym testem napisanym w Konsiście i jednym napisanym w ArchUnicie (są to entry pointy, logika reguł jest zazwyczaj w wewnętrznej bibliotece). Jestem zadowolony z takiego układu i mogę go zdecydowanie polecić.