Po co recap
Jest 15 grudnia. Mam za sobą rok wpisów o narzędziach i całkiem realną pokusę, żeby napisać dwunasty tutorial, wkleić jeszcze jeden plik YAML i zamknąć temat. Nie zrobię tego. Lista narzędzi, które przewinęły się przez ten blog od stycznia, jest do odtworzenia w dwie minuty z archiwum i nikomu nic nie daje. Changelog nie jest podsumowaniem.
Recap ma dla mnie sens tylko wtedy, kiedy zadaje pytanie, którego pojedynczy wpis zadać nie potrafi, bo siedzi za blisko swojego narzędzia. Kiedy piszę o Compose, myślę o Compose. Kiedy piszę o równoległości na agencie, myślę o agencie. Dopiero z odległości dwunastu miesięcy widać, że wszystkie te wpisy krążyły wokół jednej rzeczy, której na początku roku nie umiałem nazwać.
Pytanie roku brzmi: czy mój zestaw testów jest izolowany, czy dzieli z kimś maszynę, bazę i WebDrivera.
To jest jedno pytanie, ale odpowiada się na nie w kilku warstwach naraz i to właśnie dlatego zajęło mi rok. Można mieć świetnie napisane page objecty i dzielić bazę z kolegą z zespołu. Można mieć bazę w kontenerze i dzielić port 4444 z poprzednim przebiegiem, który nie posprzątał. Można mieć wszystko lokalnie czyste i wpaść na CI, bo agent nie jest moją maszyną. Każda z tych sytuacji wygląda jak inny problem, dopóki nie zauważy się, że to jest ta sama dziura widziana z trzech stron.
W 2019 roku pisałem o przygotowaniu środowiska pod testy i to był wpis o tym, jak coś zainstalować u siebie. Instrukcja miała sens, ale zakładała, że środowisko jest miejscem, do którego się wchodzi, a nie rzeczą, którą się tworzy na czas testu i wyrzuca. Ten rok zszedł mi na przesuwaniu tego założenia.
Dwa filary już na blogu
Dwa wpisy z tego roku niosą tę tezę mocniej niż reszta i oba są na blogu od dawna, więc nie będę ich tutaj przepisywał.
Pierwszy to kwietniowy Testcontainers. Pokazywałem w nim, jak podnieść MSSQL z poziomu kodu testu, przez bibliotekę DotNet.Testcontainers. Wtedy wszystko działo się na mojej maszynie i tak też ten wpis jest napisany: bez pipeline’u, bez agenta, z bazą, która żyje tyle co przebieg. To był moment, w którym przestałem traktować bazę jako coś, co stoi obok testu, i zacząłem traktować ją jako zależność, którą test sam sobie zamawia.
Drugi to lipcowa polityka ponawiania w testach integracyjnych, czyli Polly. I tutaj muszę powiedzieć rzecz, która brzmi jak zaprzeczenie tamtego wpisu, choć nim nie jest. Retry nie leczy braku izolacji. Polly świetnie radzi sobie z wyścigiem, który wynika z natury systemu: usługa wstaje ułamek sekundy po tym, jak ją odpytałem, kolejka ma opóźnienie, połączenie zrywa się raz na sto razy. To są rzeczy, których nie da się usunąć projektowaniem testu, bo dzieją się po drugiej stronie.
Natomiast jeżeli test jest czerwony dlatego, że inny test zostawił mu w bazie zamówienie, to ponawianie zamienia problem w problem trudniejszy do wykrycia. Test przestaje być czerwony, więc nikt go nie ogląda, a stan i tak wycieka. Zapisałem sobie to w tym roku jako regułę praktyczną: retry stosuję do rzeczy, które są niedeterministyczne z natury, i nigdy do rzeczy, które są niedeterministyczne, bo ktoś nie posprzątał.
Te dwa wpisy są dla mnie filarami roku, ale są też jego początkiem i środkiem, nie końcem. Reszta miesięcy to dokładanie warstw wokół nich.
Kontenery lokalnie
Rok zacząłem od najprostszej możliwej rzeczy. W styczniu opisałem Docker dla QA i środowiska testowe on-demand: jedna zależność, jedno docker run, Engine z linii 20.10, porty, logi i sprzątanie po sobie. Żadnej orkiestracji, żadnego pliku konfiguracyjnego. Świadomie tak wąsko, bo pierwszy próg do przeskoczenia nie jest techniczny. Chodzi o to, żeby przestać myśleć o bazie danych jako o czymś, co się instaluje raz na kwartał i potem pielęgnuje.
W lutym doszło Docker Compose do stawiania środowisk testowych. Tam pojawił się plik, kilka serwisów naraz i profile, które weszły do docker-compose w wersji 1.28. Profile okazały się dla mnie ważniejsze, niż się spodziewałem, bo pozwalają trzymać jeden opis środowiska i podnosić z niego tylko to, czego dany typ testu naprawdę potrzebuje. Test API nie musi budzić frontu.
W marcu poszedł Selenium Grid w Dockerze. Grid 3, obrazy przypięte na 3.141.59-20210311, a Grid 4 tylko jako beta obejrzana z ciekawości, nie jako coś, co wtedy stawiałem do pracy. To był ostatni element lokalnej układanki: przeglądarka też jest zależnością i też może się podnosić na żądanie zamiast mieszkać w systemie.
Kiedy patrzę na te trzy wpisy razem, widzę jedną linię: od jednej zależności, przez kilka opisanych w pliku, po zależność, która do tej pory wydawała się nieprzenośna. I widzę różnicę wobec 2019 roku. Tamten wpis o przygotowaniu środowiska mówił “zainstaluj u siebie i uważaj”. Te trzy mówią “opisz w repozytorium i wyrzuć po przebiegu”. To nie jest różnica w narzędziu. To jest różnica w tym, kto jest właścicielem środowiska: człowiek czy plik w repo.
HTTP albo prawdziwy serwis
Środkiem roku zajmowałem się drugą stroną testu, czyli tym, co odpowiada na zapytania.
W maju napisałem o mockowaniu API w kontenerach z WireMockiem. Wtedy pracowałem na WireMocku 2.28.0 i na obrazie społecznościowym rodolpheche/wiremock, bo oficjalnego obrazu od organizacji po prostu nie było. Stuby leżały w mappings/, wpinane wolumenem, i całość dawała się postawić razem z aplikacją w jednym pliku Compose.
Rzecz, którą warto odnotować na koniec roku: od 2.31.0, czyli od września, jest już oficjalny obraz wiremock/wiremock. To nie unieważnia majowego wpisu, bo w maju go nie było, i nie zamierzam go z tego powodu przepisywać. Zapisuję to jako fakt do sprawdzenia przy najbliższej okazji, kiedy będę i tak dotykał tej części konfiguracji.
Drugi biegun tej samej osi to kwietniowy Testcontainers, czyli prawdziwy serwis zamiast atrapy. I to jest wybór, który przez cały rok wracał do mnie w tej samej postaci: czy chcę szybkiej, przewidywalnej odpowiedzi, którą sam wymyśliłem, czy wolnej odpowiedzi, która jest prawdziwa.
Nie mam na to jednej reguły i nie wierzę, że taka istnieje. Mam natomiast reguły cząstkowe. Stub HTTP biorę wtedy, kiedy druga strona jest cudza, płatna albo niestabilna, i kiedy testuję zachowanie mojego kodu wobec konkretnej odpowiedzi, łącznie z odpowiedziami, których prawdziwy serwis nie chce mi wyprodukować na życzenie. Prawdziwy serwis w kontenerze biorę wtedy, kiedy testuję integrację z czymś, co mam u siebie, i kiedy kłamstwo stuba kosztowałoby mnie fałszywą zieloność. Baza danych prawie zawsze wchodzi w tę drugą kategorię, bo w SQL-u jest za dużo szczegółów, żeby udawać je JSON-em.
Uczciwe zastrzeżenie do stubów napisałem już w maju i nadal je podtrzymuję: stub to moje wyobrażenie o cudzej odpowiedzi. Jest tyle warte, ile warta jest ostatnia weryfikacja tego wyobrażenia.
Treść danych, nie tylko infra
Do września żyłem w przekonaniu, że jak uporządkuję infrastrukturę, to dane ułożą się same. Nie ułożyły się.
Wrześniowy wpis o danych testowych w .NET jest o tym, że kontener rozwiązuje pytanie “gdzie”, ale nie rozwiązuje pytania “co”. Baza wstaje pusta i ktoś musi zdecydować, co w niej będzie. Ułożyłem to sobie w cztery warstwy: fabryka w pamięci z ziarnem, wspólna baza z resetem, baza jednorazowa w kontenerze i brak bazy z odpowiedzią ze stuba.
Dwa konkrety z tamtego wpisu, które najbardziej zmieniły mi codzienność. Pierwszy to ziarno w fabryce Bogus. Losowe dane bez ziarna dają test, który raz na jakiś czas pada na czymś, czego nie umiem odtworzyć. Ziarno zamienia losowość w powtarzalną losowość, czyli w coś, o czym da się rozmawiać. Drugi to Checkpoint z Respawn, kiedy baza jest wspólna. To jest tańsze niż podnoszenie kontenera na każdy test, a daje stan, którego nikt przede mną nie dotknął.
Osobno stoi czerwiec, czyli GPT-3 API i dane testowe z Completions. Trzymam się tego, co napisałem wtedy, i po pół roku widzę to raczej wyraźniej. To był eksperyment, nie nowa warstwa strategii danych. Wołałem davinci przez Completions, promptem few-shot, po to, żeby dostać JSON-a wyglądającego jak fixture. Wychodziło z tego coś użytecznego na etapie wymyślania danych i coś zupełnie nieużytecznego jako źródło prawdy w przebiegu CI, bo dwa wywołania z tym samym promptem nie muszą dać tego samego wyniku.
Od 18 listopada API jest bez waitlisty, więc dostęp przestał być barierą. To nie zmienia dwóch pozostałych rzeczy. Nadal są to płatne Completions, czyli wywołanie tekst na wejściu, tekst na wyjściu, a nie interfejs, z którym się rozmawia. I nadal każdy wygenerowany fixture przechodzi u mnie przez przegląd, zanim trafi do repozytorium. Generator pomaga mi wymyślić dwadzieścia wariantów adresu. Nie decyduje, który z nich jest poprawny.
Kod UI
Sierpień był miesiącem, w którym nie stawiałem niczego w kontenerze. Refaktor POM w C# dotyczył kodu testów, na Selenium 3.141.0 i NUnit 3.13.
Trzy decyzje z tamtego wpisu zostały ze mną. Modeluję fragmenty strony w rozumieniu Fowlera, a nie całe strony, bo tłusta klasa strony rośnie razem z aplikacją i po roku nikt jej nie czyta. Nie używam PageFactory, bo w .NET jest oznaczone jako przestarzałe od dawna i nie ma powodu budować na nim nowego kodu. Driver żyje w zasięgu testu, a nie jako pole statyczne dzielone przez cały przebieg, bo to jest dokładnie ta sama historia o izolacji, tylko na poziomie procesu zamiast bazy.
Warto to zestawić z tym, co pisałem w 2019 przy Cypressie, gdzie porównywałem App Actions z Page Object Model. Tamten wpis nie jest nieaktualny i nie zamierzam go przepisywać na C#. Konteksty są inne: w Cypressie mogę zejść pod UI i ustawić stan aplikacji z poziomu testu, w Selenium raczej nie. Ta sama nazwa wzorca znaczy w tych dwóch światach co innego i to jest w porządku.
Osobno zostawiam temat selektorów, bo o poprawnym używaniu selektorów i atrybucie data-cy napisałem w 2019 i przez dwa lata nie zmieniłem zdania. Stabilny atrybut dodany do markupu specjalnie dla testów jest tańszy niż najlepszy XPath świata.
CI
Ostatnie dwa miesiące roku poszły na CI i to nie przypadek, że dopiero teraz.
Październikowy wpis o równoległych testach na CI rozdziela trzy poziomy, których mieszanie kosztowało mnie wcześniej sporo czasu: wątki w jednym procesie po stronie frameworka, procesy na jednej maszynie po stronie VSTest oraz osobne joby na wielu agentach, czyli plasterki w Azure Pipelines albo strategy.matrix w GitHub Actions. Każdy z tych poziomów ma inną jednostkę podziału i inne rzeczy, które przez przypadek dzieli.
Listopadowy wpis o Testcontainers w Azure Pipelines domyka kwiecień. Na hostowanym agencie ubuntu-20.04 Docker jest na hoście i biblioteka ma z kim rozmawiać. Pułapka, na którą tam trafiłem i o której warto pamiętać, to container job: sam mechanizm jest w Azure DevOps od 2018 roku i nie jest żadną nowością, ale kiedy mój krok wykonuje się wewnątrz kontenera, to nie ma w nim zagnieżdżonego demona, a Testcontainers nie ma do czego się podłączyć.
Kolejność tych dwóch wpisów jest dla mnie ważniejsza niż ich treść. Najpierw izolacja, potem wall-clock. Równoległość jest wzmacniaczem: bierze to, co masz, i mnoży przez liczbę workerów. Jeżeli masz czysty stan, dostajesz szybszy zestaw. Jeżeli masz wyciekający stan, dostajesz szybszy szum i zespół, który uczy się klikać “rerun”.
Co świadomie zostawiam na później
Trzy rzeczy istnieją, a ja i tak ich w tym roku nie wziąłem, i chcę to zapisać jawnie, bo za rok nie będę pamiętał, że to była decyzja, a nie przeoczenie.
Compose w wersji drugiej, czyli docker compose bez myślnika, pojawił się w tym roku w pierwszych wydaniach. Wszystkie moje tegoroczne wpisy uczą docker-compose z myślnika, w linii 1.27 i 1.28, i tak też mam ustawione lokalnie oraz na agentach. Nie chcę pisać tutoriala o codziennym CLI, którym sam jeszcze nie posługuję się codziennie, i nie chcę zmieniać polecenia w dokumentacji zespołu w połowie roku. Przejście zrobię, kiedy będzie oczywiste, a nie kiedy będzie możliwe.
Selenium 4.0.0 wyszło 13 października i to jest realna zmiana, nie kosmetyka. Mój zestaw stoi nadal na Selenium.WebDriver 3.141.0 i tak zamykam rok. Migracja klienta to osobna robota z własnym ryzykiem, a grudzień jest złym miesiącem na wymianę fundamentu pod zestawem regresji.
Trzecia rzecz to interfejs do modelu językowego. Mam Completions i traktuję je jak API, którym są: wołam, dostaję tekst, sprawdzam. Nie buduję wokół tego rozmowy, nie wpinam tego w przebieg testów i nie robię z tego warstwy generowania danych na produkcji zestawu. To pozostaje eksperymentem i chcę, żeby nazwanie tego wprost przetrwało w archiwum bloga.
Jedno zdanie na 2022
Jedno zdanie: w przyszłym roku chcę mierzyć flake rate i czas przebiegu zestawu, a nie liczbę narzędzi, które umiem postawić.
To jest intencja, nie raport, i nie mam pojęcia, jak wyjdzie. Ale wiem, dlaczego akurat te dwie liczby. Obie mówią o tym, czy komuś opłaca się czekać na zielone. Liczba narzędzi w repozytorium nie mówi o tym nic, a jest przyjemniejsza do pokazania, więc łatwo się nią podmienia prawdziwy wynik.
Druga rzecz, którą chcę zabrać do stycznia, jest z 2019 roku. Opisywałem wtedy proces testowy, do jakiego dążę w projektach, i po tym roku widzę to samo z innej strony. Kontener, stub, ziarno w fabryce i matrix na CI to są narzędzia. Ustalenie, kto ogląda czerwony wynik i co się dzieje w ciągu godziny od jego pojawienia się, to jest proces. Rok 2021 zszedł mi po stronie narzędzi i nie żałuję, bo bez izolacji nie ma o czym rozmawiać. Ale narzędzia mam już poukładane na tyle, że dalsze dokładanie ich do stosu przestało być najtańszą poprawą.
Do zobaczenia w styczniu.

