Wszystkie wpisy

Piramida testów i AI - wrzesień 2023
Piramida testów i AI - wrzesień 2023

English

Piramida testów i AI - wrzesień 2023

Wrzesień 2023: czat, Copilot i experimental CT przyspieszają pisanie testów. Piramida i triggery CI zostają. Chat nie jest jeszcze public beta dla wszystkich.

Nie pytam o narzędzie roku, tylko o proporcje

Jest 15 września. Od stycznia mam obok edytora otwartą kartę z czatem, od marca przełącznik z GPT-4 w abonamencie, od lipca dostęp do API gpt-4 bez waitlisty, a od wiosny czat w samym edytorze. Napisałem o tym w tym roku cztery wpisy: o pierwszych przypadkach testowych z czatu, o strukturze promptu, o GPT-4 dzień po premierze i o recenzowaniu sugestii Copilota. Wszystkie odpowiadały na to samo pytanie: czy to narzędzie skraca mi konkretną czynność przy biurku.

Dziewięć miesięcy to dość, żeby zadać pytanie o poziom wyżej, i to jest jedyne pytanie tego wpisu. Nie “które narzędzie jest najlepsze”, tylko: czy po tych dziewięciu miesiącach zmieniły się proporcje mojego zestawu testów. Czy mam mniej E2E, bo model pomaga mi pisać testy. Czy w ogóle pojawiła się jakaś nowa warstwa, której rok temu nie było.

Zaznaczam od razu, żeby nie było nieporozumienia co do gatunku tego tekstu. Piramida testów nie jest odkryciem 2023 roku ani moim pomysłem. Mike Cohn opisał ją w 2009 roku, Martin Fowler spopularyzował notatką w 2012, a przez dekadę zdążyła się dorobić własnych krytyk, trofeów i innych kształtów. Nie proponuję tu nowego frameworka, nie nazywam niczego po sobie i nie wymyślam szczebla “testy LLM”. Sprawdzam tylko, czy stary rysunek nadal opisuje to, co realnie mam w repozytorium.

W podsumowaniu 2022 zapisałem, że AI weszło u mnie do edytora jako przewidywanie kolejnej linii i nie weszło nigdzie indziej. To zdanie przestało być prawdziwe gdzieś w lutym. Warto natomiast zobaczyć, czym dokładnie przestało być prawdziwe, bo różnica między “piszę testy szybciej” a “potrzebuję innych testów” jest całym tym wpisem.

Liczby zbieram na tym samym publicznym demie w stylu RealWorld, którego używam na blogu od 2021 roku: logowanie, nagłówek zalogowanego użytkownika, lista artykułów, kilka endpointów po stronie .NET. Nie ma tu ani jednej liczby z projektu klienta i nie będzie. Runner mam przypięty na 1.38.0 z 13 września, backend na .NET 7.

Policzyłem pliki i metody testowe w dwóch momentach: w grudniu, kiedy pisałem podsumowanie roku, i w zeszły weekend.

Warstwa Grudzień 2022 Wrzesień 2023
Jednostkowe (C# i TypeScript) 214 331
Integracyjne z kontenerem i API 63 88
Kontraktowe (konsument i dostawca) 11 14
Komponentowe (CT, experimental) 0 17
E2E przez przeglądarkę 26 24
Porównania zrzutów ekranu 9 9
Razem 323 483

Udział E2E spadł z ośmiu procent do pięciu. Brzmi jak spłaszczona piramida i nie jest nią, bo mianownik urósł o sto sześćdziesiąt testów, a licznik nie urósł wcale. Nie usunąłem ani jednego scenariusza E2E dlatego, że mam model. Trzy, które zniknęły, wypadły z powodu usuniętej funkcji w demie, a jeden nowy doszedł, kiedy dokładałem koszyk. To jest cała zmiana proporcji: dół urósł szybciej, niż byłem to w stanie zrobić rok temu, i tyle.

Co AI faktycznie robi na dole piramidy

Dół piramidy to warstwa, na której AI zarabia u mnie najwięcej, i to jest zarazem warstwa, która była tania już wcześniej. To zestawienie jest ważniejsze, niż wygląda: przyspieszenie dostałem tam, gdzie i tak nie bolało.

Testy jednostkowe i klasy pomocnicze piszę z ghost textem Copilota i nie zmieniło się to od zeszłego roku. Kolejne [InlineData], przeciążenie buildera, mapowanie DTO na model, pusta klasa page objectu z listą lokatorów do wypełnienia. Tam przyjmuję sugestię prawie bez poprawek, bo kształt jest powtarzalny i nie zależy od zachowania mojej aplikacji. Sto siedemnaście nowych testów jednostkowych z tabeli powyżej powstało w większości tak: ja piszę nazwę i pierwszą asercję, edytor dopisuje resztę wiersza, ja czytam. Filtr się nie zmienił i opisałem go szczegółowo w kwietniu: asercja, dane, sprzątanie, selektor, którego nie ma.

Dane testowe generuję z API gpt-3.5-turbo. Od czerwca proszę o wynik przez function calling, więc dostaję JSON zgodny ze schematem, a nie akapit z JSON-em w środku, co skróciło mi parsowanie do zera. Zasada z 2021 roku nie ruszyła się ani o przecinek: wynik generowania jest wejściem do przeglądu, a nie fixture’em. Zatwierdzony plik ląduje w repozytorium i to on jedzie na CI. Model nie jest wołany podczas przebiegu testów i wrócę do tego przy triggerach.

Szkice przypadków testowych robię w czacie, tak jak w styczniu, tylko sprawniej, bo mam wypracowane prompty i wiem, czego się nie da z tego wycisnąć. Tu jedna rzecz wymaga precyzji, bo we wrześniu 2023 łatwo napisać nieprawdę. Czat w edytorze mam z technical preview, na które zapisałem się w marcu i które przyszło wiosną. To nadal jest preview, a nie funkcja, którą każdy subskrybent Copilota włącza sobie w ustawieniach. Organizacje na planie firmowym dostały w lipcu publiczną betę czatu, ale to jest osobna ścieżka i osobny plan, nie moje konto indywidualne. Piszę to, bo widzę w sieci teksty sugerujące, że czat “jest już dla wszystkich”, i po prostu nie jest, przynajmniej nie w dniu, w którym to publikuję.

Teraz sedno tej sekcji. Wszystkie trzy rzeczy powyżej produkują wejście do warstwy, która była najtańsza w całym zestawie: test jednostkowy chodzi w milisekundach, nie potrzebuje sieci, nie potrzebuje przeglądarki, nie migocze. Jeżeli narzędzie obniża koszt produkcji testów w warstwie, która i tak była najtańsza w wykonaniu, to logiczną konsekwencją nie jest mniej E2E. Konsekwencją jest szerszy dół, bo nagle opłaca się napisać dwadzieścia przypadków brzegowych dla parsera kwot, na które wcześniej szkoda było popołudnia. Piramida robi się bardziej piramidą, nie mniej.

Nie znaczy to, że mam mniej pracy. Przeniosłem ją z pisania na czytanie i to jest cała transakcja, którą zawieram od stycznia. Dwadzieścia wygenerowanych przypadków brzegowych trzeba przeczytać co do jednego, bo trzy z nich będą duplikatami, jeden zawoła Assert.NotNull na czymś, co nigdy nie jest puste, a jeden trafi w regułę biznesową, której w demie nie ma.

Środek piramidy nie ma skrótu

Środek to u mnie trzy rzeczy: kontrakty, integracja na hoście in-memory i integracja z prawdziwą zależnością w kontenerze. To jest warstwa, na której model pomaga najmniej, i warto powiedzieć dlaczego, bo powód jest strukturalny, a nie chwilowy.

Test kontraktowy, który opisywałem we wprowadzeniu do contract testingu, jest zapisem umowy między dwoma zespołami. Model może mi wygenerować szkielet interakcji i nawet trafnie zgadnąć nazwy pól, bo takie API widział tysiąc razy. Nie może zrobić jednej rzeczy, która nadaje temu testowi sens: nie może uzgodnić z drugą stroną, że tak właśnie ma wyglądać odpowiedź, i nie ponosi konsekwencji, kiedy dostawca ją zmieni. Pakt jest dokumentem z dwoma podpisami. Wygenerowany pakt bez rozmowy to zgadywanka zapisana w pliku JSON, czyli dokładnie ten rodzaj artefaktu, który wygląda jak proces i nim nie jest.

Podobnie z testami na hoście in-memory, które opisywałem przy testach integracyjnych w .NET. Copilot dopisuje mi konfigurację fabryki i podmianę rejestracji w kontenerze zależności całkiem sprawnie, bo to jest kod ceremonialny. Wartość tego testu siedzi natomiast w wyborze, co podmieniam, a czego nie, i ten wybór wynika z wiedzy o tym, co w tej aplikacji jest ryzykowne. Model nie ma dostępu do tej wiedzy, bo jej nie ma w kodzie.

Kontenery zostają dokładnie tam, gdzie były, i z tych samych powodów co rok temu. Zimny start bazy to nadal grube kilkadziesiąt sekund i żaden model tego nie skraca, bo to jest czas startu procesu w obrazie, a nie czas mojego pisania. Mechanikę spisałem przy Testcontainers i Playwrighcie w pipelinie i nie mam do tamtego tekstu żadnej poprawki po roku AI.

Jest za to jedna rzecz, w której model pomógł mi w środku piramidy realnie, i nie jest to generowanie testów. Kiedy dostaję czerwony przebieg z długim stack trace’em z warstwy integracyjnej, wklejam log do czatu i proszę o hipotezy, od czego zacząć. To jest praca ze zrzutem błędu, nie z kodem produkcyjnym, i muszę przy tym pilnować tego samego, co pilnuję od stycznia: nie wysyłam cudzych danych ani logów z firmowego pipeline’u. Trafność takich hipotez jest mniej więcej taka jak trafność kolegi, który zna framework, ale nie zna mojej aplikacji, czyli przydatna przy pierwszej rundzie i bezużyteczna przy trzeciej.

UI: komponent, E2E i zrzut ekranu to trzy różne koszty

Nad środkiem mam trzy rzeczy, które łatwo zlać w jedno zdanie o “testach UI”, i tego zlania chcę uniknąć, bo mają zupełnie inne ceny.

Component testing w Playwrighcie próbowałem w sierpniu i stąd siedemnaście testów w tabeli. Montuję pojedynczy komponent Reacta w prawdziwej przeglądarce, bez stawiania całej aplikacji i bez logowania. Jedna rzecz, o której trzeba pamiętać przy każdym zdaniu na ten temat: pakiet nazywa się @playwright/experimental-ct-react i słowo experimental w nazwie nie jest ozdobą. Nie ma GA, API może pęknąć przy zmianie wersji minorowej, więc trzymam dokładny pin i podnoszę go na osobnym branchu. To nie jest warstwa, którą dziś polecam komuś jako fundament strategii. To jest warstwa, którą testuję na własnym demie.

E2E przez przeglądarkę zostaje tam, gdzie było, i nadal kosztuje tyle samo. Dwadzieścia cztery scenariusze, pełna macierz trzech silników, wall-clock liczony w dziesiątkach minut. Ani czat, ani mount komponentu nie zdejmują z tego zestawu jednej rzeczy, dla której on istnieje: sprawdzenia, że logowanie, sesja, przekierowanie i nagłówek działają razem, w prawdziwej przeglądarce, na złożonej aplikacji. Testy komponentowe nie widzą routingu ani autoryzacji, bo z definicji montują kawałek.

Porównania zrzutów ekranu stoją na dziewięciu i celowo nie rosną. Macierz narzędzi rozpisałem przy narzędziach do regresji wizualnej i nic z tamtych wniosków nie unieważniłem. Zrzut z toHaveScreenshot odpowiada na pytanie, którego nie zadaje ani CT, ani E2E: czy to nadal wygląda tak samo. Jest przy tym najbardziej wrażliwy na fonty, animacje i wersję przeglądarki, więc każdy dołożony zrzut to przyszły koszt utrzymania, a nie darmowa asercja.

Wygodnie mi zapisać to jako trzy pytania, bo wtedy widać, że żadne z nich nie zastępuje pozostałych. CT pyta, czy komponent zachowuje się poprawnie w izolacji. E2E pyta, czy ścieżka działa w złożeniu. Zrzut pyta, czy wygląd się nie zsunął. Model przyspiesza pisanie każdego z tych trzech i nie zmienia ani jednej z tych trzech odpowiedzi.

Triggery zostają bez zmian

Rok temu rozdzieliłem zestaw na trzy koszyki po triggerze i opisałem to w strategii testów w CI/CD: pull request bierze to, co szybkie i deterministyczne, main bierze integrację z prawdziwymi zależnościami, noc bierze pełne E2E i macierz. Po dziewięciu miesiącach z AI nie zmieniam w tym podziale ani jednej linii i to jest chyba najmocniejszy wniosek tego wpisu.

Nowe testy jednostkowe wchodzą do koszyka pierwszego, bo są szybkie i deterministyczne, i to jest jedyny powód. To, że powstały z pomocą modelu, nie daje im ani przyspieszenia, ani ulgi w wymaganiach. Bramka na pull requeście urosła mi przez to z siedmiu minut do dziewięciu i uważam to za dobrze wydane dwie minuty, bo w zamian dostałem sto siedemnaście asercji, które wcześniej po prostu nie istniały.

Testy komponentowe wsadziłem do koszyka drugiego, czyli na main po merdżu, i nie do bramki. Nie dlatego, że są wolne, bo nie są. Dlatego, że pakiet jest experimental i nie chcę, żeby zmiana wersji minorowej w cudzej paczce blokowała komuś merge. Kiedy przestanie być experimental, przeniosę je i będzie to jedno zdanie w pliku YAML.

Model nie dostaje własnego joba i nie dostaje własnego kroku w pipelinie. Powtórzę argument z 2021 roku, bo nadal jest jedynym, którego potrzebuję: dwa identyczne wywołania nie muszą dać tego samego wyniku, więc krok zależny od odpowiedzi modelu jest migotliwy z definicji, a czerwony przebieg zaczyna znaczyć “cudze API miało zły dzień” zamiast “aplikacja jest zepsuta”. W kwietniu zapisałem tę samą decyzję dla generowanego opisu pull requesta i nie widzę powodu, żeby ją odwoływać. Wygenerowany tekst nie wykonuje kodu, więc nie może być bramką.

Jest jeszcze pokusa, którą warto nazwać, bo widzę ją u siebie. Kiedy narzędzie robi wrażenie, chce się dać mu miejsce w pipelinie na dowód, że się je docenia. To jest odwrotna kolejność. Miejsce w pipelinie należy się temu, co odpowiada na pytanie, którego inaczej nie da się zadać, i co odpowiada tak samo za każdym razem. Model nie spełnia drugiego warunku, a pierwszego nie spełnia dlatego, że jego produkt, czyli kod testu, jest już w repozytorium i jedzie zwykłym runnerem.

Podsumowanie

Po dziewięciu miesiącach wniosek mieści się w jednym zdaniu: AI przyspieszyło produkcję testów i nie zmieniło ich rozkładu.

Rozbijam to na trzy zdania, które chcę móc przeczytać za rok. Dół piramidy urósł o sto siedemnaście testów jednostkowych, a cały zestaw o sto sześćdziesiąt, bo pisanie ich stało się tańsze, więc udział E2E spadł z ośmiu procent do pięciu bez usunięcia ani jednego scenariusza E2E z powodu modelu. Środek, czyli kontrakty i integracja, nie przyspieszył prawie wcale, bo jego kosztem nie jest pisanie, tylko uzgadnianie z drugą stroną i czekanie na prawdziwe zależności. Góra kosztuje tyle samo co rok temu i chodzi na tym samym triggerze, bo nocna macierz przeglądarek nie stała się szybsza od tego, że asercję w niej podpowiedział mi edytor.

Czego z tego nie wynika. Nie wynika, że mam nową warstwę o nazwie “testy LLM”, bo jej nie mam i nie widzę dla niej miejsca. Nie wynika, że każdy ma dziś czat w edytorze, bo u mnie to nadal preview z marcowej waitlisty, a nie funkcja włączana z poziomu ustawień konta indywidualnego. Nie wynika też, że component testing jest gotowe na całą organizację, bo pakiet ma experimental w nazwie i tyle o tym.

Najkrótsza wersja tego, co zrozumiałem w tym roku, brzmi tak: AI jest taśmą produkcyjną, a nie nowym szczeblem. Zmienia tempo, w jakim powstaje test, i nie zmienia ceny jego wykonania, bo cena wykonania siedzi w przeglądarce, w kontenerze i w sieci, a nie w moim pisaniu. Piramida była zawsze rysunkiem o cenie wykonania i o szybkości sprzężenia zwrotnego. Dlatego zostaje.

Wracam więc do pytania, które zadaję przy każdym nowym narzędziu od 2019 roku: gdzie to siedzi w całym procesie testowym. Sto siedemnaście nowych testów jednostkowych nie naprawia procesu, w którym nikt nie czyta czerwonego wyniku na pull requeście. Sprawia tylko, że mam więcej rzeczy do przeczytania, kiedy ten wynik wreszcie coś wykryje. Za trzy miesiące napiszę podsumowanie roku i wtedy sprawdzę, czy proporcje z tabeli w ogóle drgnęły, czy tylko urosły wszystkie wiersze naraz.