Przejdź do treści

Ile testów ma produkcyjna aplikacja? Liczby z zawodowe.edu.pl

W serii o testowaniu obiecałem na koniec liczby zamiast deklaracji. Oto one – policzone w kodzie zawodowe.edu.pl, czyli platformy do egzaminów zawodowych, z której korzystają tysiące osób miesięcznie. Projekt składa się z trzech części: backendu w Django (który serwuje też API), aplikacji mobilnej w React Native oraz testów przepływów spinających jedno i drugie.

Jak liczyłem – żeby dało się to powtórzyć

Zasada z tego bloga: liczba, której nie da się odtworzyć, jest ozdobnikiem. Więc metoda wprost: linie kodu to wc -l po plikach źródłowych, z pominięciem zależności, migracji baz danych i wygenerowanych katalogów. Liczba testów to zliczenie definicji testów w plikach testowych (def test_ w Pythonie, bloki it( w TypeScripcie) – statycznie, bez uruchamiania. To liczenie ostrożne: testy parametryzowane liczą się raz, choć w trakcie wykonania rozwijają się w wiele przypadków, więc realna liczba wykonywanych testów jest wyższa od podanych niżej.

Backend: Django

CoIle
kod aplikacji (Python)101 427 linii w 561 plikach
szablony HTML45 042 linie w 339 plikach
pliki testów493
testy (definicje)6 621
kod testów105 532 linie

Najpierw proporcja, bo ona mówi najwięcej: na każdą linię kodu aplikacji przypada dziś nieco ponad linijka testów (dokładnie 1,04). Obok właściwego projektu istnieje drugi, tej samej wielkości – a właściwie już odrobinę większy – niewidoczny dla użytkownika, i jego jedynym zadaniem jest pilnowanie pierwszego.

Suita dzieli się zgodnie z piramidą z drugiej części serii, choć nie jest podręcznikowo piramidalna: 1 642 testy jednostkowe, 4 461 integracyjnych, 220 testów nowego interfejsu i 298 testów przepływów. Środek jest grubszy od podstawy – i to jest świadome. W aplikacji Django większość błędów, które naprawdę bolą, mieszka w umowach między widokiem, zapytaniem i szablonem, więc tam siedzi najwięcej asercji. Piramida to zasada ekonomii, nie dogmat kształtu.

Osobny smaczek to te 220 testów interfejsu – w większości testy-strażnicy, o których pisałem przy wyprowadzaniu Bootstrapa. Nie sprawdzają, czy coś działa – sprawdzają, czy obowiązują decyzje: każda klasa CSS w nowych szablonach musi mieć przepisany prefiks albo być na krótkiej liście wyjątków, data-bs- i style zapisane w atrybutach są zakazane, a do tego dochodzą asercje na kontrast kolorów i nieużywane klasy. Backend serwuje też API dla aplikacji mobilnej – 99 endpointów – i one wchodzą w tę samą suitę integracyjną co widoki HTML.

Mobilka: React Native i Expo

CoIle
kod aplikacji (TS/TSX)43 533 linie w 258 plikach
pliki testów133
testy (bloki it)ponad 1 600
kod testów26 824 linie
pokrycie linii55,5% (gałęzi: 41,3%)

Proporcja skromniejsza niż na backendzie – 0,6 linii testu na linię kodu – i pokrycie, którym nie ma się co chwalić na sztandarze. Ale to pokrycie jest świadomie nierówne: warstwy logiki – baza danych, hooki, komunikacja z API – siedzą w okolicach 85%, a zaniża je warstwa ekranów, której nie testuję przez renderowanie całych widoków. Zamiast tego logika jest wyciągana z ekranów do osobnych modułów i testowana tam. Ekran, w którym zostało samo układanie komponentów, psuje się rzadko – a jego test kosztowałby najwięcej ze wszystkich.

Przy okazji tego projektu licznik pokrycia sam dostarczył lekcję pokory, dokładnie w duchu rozdziału o religii pokrycia: domyślnie Jest liczył pokrycie wyłącznie z plików dotkniętych przez testy. Raport pokazywał 79%, rzeczywistość – po wskazaniu całego kodu aplikacji jako podstawy – 38%, bo najgrubszy katalog miał okrągłe zero i nie było go widać. Wynik na liczniku bywa fikcją nie dlatego, że ktoś oszukuje, tylko dlatego, że nikt nie sprawdził, co licznik w ogóle mierzy.

Mobilka ma też swoich testów-strażników – i każdy powstał tak samo: coś zepsuło się raz naprawdę. Test pilnujący, żeby żaden adres API nie zawierał parametrów w query stringu (bo warstwa ochronna przed serwerem ucinała takie żądania), test motywu systemowego, test transakcyjności zapisu do lokalnej bazy, testy dostępności. To są blizny z pierwszej części serii – tyle że zamienione w zasieki.

Przepływy: Playwright i Maestro

Szczyt piramidy, opisany w części trzeciej, w liczbach wygląda tak: na webie 298 testów Playwrighta w 50 plikach – od pełnych przebiegów egzaminu w kilku wariantach, przez rejestrację, logowanie i reset hasła, po panel nauczyciela i skrzynkę wiadomości. Na mobile 7 przepływów Maestro plus 3 podprzepływy wielokrotnego użytku (logowanie czy wybór kwalifikacji są dołączane przez runFlow, żeby scenariusze się nie dublowały – zasada „nie powtarzaj się” obowiązuje także w YAML-u): dymne testy dla trzech ról użytkownika, pełny egzamin, tryb nauki, ćwiczenia, panel nauczyciela.

Zgodnie z ekonomią piramidy to ułamek całości – niecałe 5% wszystkich testów – ale to ten ułamek odpowiada na jedyne pytanie, którego nie zada żaden test jednostkowy: czy serwis, jako całość, robi to, po co istnieje.

Bramka przed produkcją

Najważniejsza liczba tego wpisu nie ma jednak nic wspólnego z pokryciem. Wdrożenie backendu przechodzi przez GitHub Actions i jest bramkowane w kolejności: najpierw audyt zależności pod kątem znanych podatności, potem analiza statyczna bezpieczeństwa, potem suita testów jednostkowych i integracyjnych z progiem pokrycia – ponad sześć tysięcy testów; przepływy Playwrighta biegną osobno, poza bramką, bo wymagają uruchomionej aplikacji. Dopiero po zielonym komplecie kod jedzie na serwer. Sam deploy odpalam ręcznie – automat pilnuje jakości, ale decyzję o dotknięciu produkcji podejmuje człowiek.

I teraz sedno, dla którego ten rozdział istnieje: ta bramka nie raz zatrzymała wdrożenie, które położyłoby serwis. Zmiana wyglądała niewinnie, lokalnie „przecież działało” – a czerwony pasek w Actions mówił: nie tym razem. Latami o takich błędach informowała mnie produkcja – logami, użytkownikami albo najgorszą z opcji, ciszą. Dziś informuje mnie czerwony build, który nie kosztuje nikogo nic poza kwadransem poprawki. Dlatego przestałem traktować czerwony build jak porażkę. Czerwony build to test, który właśnie zarobił na swoje utrzymanie.

Sekcja uczciwości: czego te liczby nie mówią

  • Pełnego pokrycia nie ma i nie planuję. 41% pokrycia gałęzi w mobilce to nie jest liczba do oprawienia w ramkę – to mapa miejsc, gdzie nikt nie pilnuje. Część z nich to świadoma decyzja (ekrany), część to zwykły dług.
  • Asymetria CI jest realna. Backend ma automatyczną bramkę przed każdym wdrożeniem; mobilka nie ma żadnej – testy i przepływy Maestro biegną przed wydaniem, ale uruchamiane ręcznie, z dyscypliny, nie z automatu. Częściowo to natura rzeczy (web wdraża się wielokrotnie w tygodniu, aplikacja wychodzi wydaniami przez sklepy), ale częściowo po prostu słabszy punkt tego układu.
  • Liczba testów niczego nie dowodzi. 6 621 testów, które sprawdzają nie to, co trzeba, jest warte mniej niż 50 sprawdzających granice i umowy. Liczby z tego wpisu mają sens dopiero razem z zasadami z serii – bez nich to tylko duży licznik.

Puenta

Zbierając wszystko: około 145 tysięcy linii kodu aplikacji pilnuje ponad 132 tysiące linii testów – niemal drugi projekt tej samej wielkości, którego żaden użytkownik nigdy nie zobaczy. Zbyt długo skąpiłem na jego budowie i płaciłem zamiast tego rachunki z produkcji: godziny szukania przyczyn po objawach, zaufanie użytkowników, funkcje leżące po kilka dni. Ten drugi projekt też wystawia faktury – każdy test trzeba napisać i utrzymać – ale po obu stronach tego rachunku siedziałem wystarczająco długo, żeby wiedzieć, która faktura jest niższa.