Diagram workflow opartego na LLM do diagnozowania i stabilizowania testów E2E w Playwright
12 min czytania

Jak zbudowałem workflow oparty na LLM, który ustabilizował testy E2E w prawdziwym projekcie

AI Systems Quality

Jak zamiana wiedzy zespołu w wykonywalny workflow zmniejsza flakiness, przyspiesza diagnozę i utrzymuje zaufanie do rosnącego pakietu regresji.


Flaky testy E2E to cisi zabójcy tempa zespołu.

Marnują minuty CI, generują fałszywe alarmy, spowalniają wydania i stopniowo niszczą zaufanie do całego pakietu testów. Przez ostatni miesiąc nie tylko pisałem testy w Playwright. Budowałem workflow oparty na LLM, który pomagał diagnozować awarie, ograniczać flakiness i utrzymywać spójny standard testów regresyjnych w prawdziwym projekcie produkcyjnym.

W tym projekcie używamy Playwrighta, testów regresyjnych, integracji z Qase oraz kilku flow, które z natury trudno testować: logowania, modali, płatności, iframe’ów, użytkowników o różnych stanach konta, dynamicznych odpowiedzi aplikacji, zewnętrznych zależności i CI.

Na początku LLM pomagał mi punktowo: „Dlaczego ten test się wywala?”, „Jak poprawić ten selektor?”, „Jak napisać tę asercję?”. Szybko stało się jasne, że to za mało.

Prawdziwy przełom nastąpił, gdy przestałem traktować LLM jak generator kodu i zacząłem traktować go jak uczestnika procesu diagnostycznego, który musi trzymać się lokalnych reguł projektu.


Problem: flaky E2E to nie jeden typ buga

Flaky testy rzadko mają jedną przyczynę.

Czasem test klika za wcześnie. Czasem modal blokuje ekran. Czasem selektor był oparty na tekście, który zmienił się po refaktorze. Czasem networkidle nigdy nie nadchodzi, bo aplikacja robi polling w tle. Czasem test przechodzi lokalnie, ale w CI trafia na wolniejszy backend, rate limit albo opóźniony rendering.

Najgorsze jest to, że szybkie „fixy” często tylko przenoszą problem gdzie indziej.

Źle:

await page.waitForTimeout(3000)

Test zaczyna przechodzić. Na jakiś czas.

Potem CI zwalnia, backend odpowiada później, modal pojawia się sekundę później i test znów staje się flaky. Tyle że teraz trudniej go zdiagnozować, bo jest naszpikowany magicznymi sleepami.

Potrzebowałem czegoś więcej niż poprawiania testów. Potrzebowałem powtarzalnego sposobu pracy.


Rezultaty

To nie był eksperyment na zabawkowym repozytorium. Workflow działał na prawdziwym pakiecie regresji połączonym z Qase i Playwrightem.

W momencie podsumowania:

  • 161 regresyjnych testów E2E było powiązanych z przypadkami testowymi w Qase przez QaseID,
  • tylko 2 przypadki Qase o wysokim priorytecie wciąż nie miały implementacji E2E,
  • powstały reużywalne skille do pisania, debugowania i przeglądania testów E2E,
  • reguły anty-flake’owe przeniosłem z „pamięci w czyjejś głowie” do plików wiedzy w repozytorium,
  • diagnoza awarii stała się bardziej ustrukturyzowana: znajdź test, uruchom go, sklasyfikuj błąd, przejrzyj powiązany kod, wyjaśnij pierwotną przyczynę i dopiero potem zastosuj poprawkę.

Celowo nie twierdzę, że LLM „usunął wszystkie flaky testy”. To byłoby nieuczciwe. Prawdziwy zysk był inny: mniej przypadkowych poprawek, szybsza diagnoza i powtarzalny standard pracy z E2E.


Pierwsza lekcja: LLM bez kontekstu projektu produkuje kruche rozwiązania

LLM dobrze zna Playwrighta. Ale nie zna mojego projektu.

Nie wie, które helpery już istnieją. Nie wie, że w tym repo nie używamy networkidle. Nie wie, że negatywne asercje widoczności muszą przechodzić przez dedykowany helper. Nie wie, że testy regresyjne wymagają QaseID. Nie wie, które flow trzeba serializować, bo dotyka zewnętrznego sandboxa płatności.

Jeśli podamy mu tylko błąd z terminala, często dostajemy fix, który w oderwaniu wygląda rozsądnie, ale nie pasuje do systemu.

Dlatego zacząłem budować lokalny workflow oparty na dwóch elementach:

  • skille do konkretnych zadań,
  • pliki wiedzy z regułami testowania E2E.

Gdzie i jak to wdrożyłem

Workflow wdrożyłem bezpośrednio w repozytorium, a nie jako osobną aplikację LangChain, własny plugin do VS Code czy samodzielny wrapper CLI.

Wykorzystałem skille agenta na poziomie repozytorium oraz pliki wiedzy:

.agents/
  skills/
    fix-e2e/
      SKILL.md
    regression-e2e/
      SKILL.md
  knowledge/
    e2e-testing.md
    e2e-regression.md

Skille to instrukcje w Markdownie, z których coding agent korzysta podczas pracy w repo. Definiują zachowanie dla konkretnego zadania: jak debugować wywalający się test, jak napisać test regresyjny, które pliki czytać najpierw, których helperów użyć i których reguł anty-flake’owych pilnować.

Pliki wiedzy działają jak pamięć projektu. Zawierają konwencje Playwrighta, katalogi helperów, antywzorce, reguły nazewnictwa, konwencje Qase oraz przykłady z prawdziwej bazy kodu.

Cały setup był celowo prosty:

  • żadnego własnego frameworka do orkiestracji LLM,
  • żadnego osobnego serwisu,
  • żadnego fine-tuningu,
  • żadnej skomplikowanej infrastruktury agentowej.

Po prostu lokalne instrukcje w repozytorium, osadzone w bazie kodu i używane konsekwentnie podczas pracy z E2E.


Skill /fix-e2e: LLM jako debugger, a nie maszyna do zgadywania

Pierwszy ważny skill skupiał się na naprawianiu wywalających się testów E2E.

Założenie było proste: LLM nie powinien od razu edytować testu. Najpierw musi przejść przez procedurę diagnostyczną.

Workflow wyglądał z grubsza tak:

  1. Znajdź testpo nazwie pliku, ścieżce lub tytule.
  2. Przeczytaj cały testod początku do końca, zanim postawisz jakąkolwiek hipotezę.
  3. Uruchom go lokalnieaby odtworzyć awarię na maszynie developera.
  4. Sklasyfikuj awarięasercja, selektor, race, błąd runtime, środowisko.
  5. Przeczytaj powiązany kodhelpery, komponenty lub logikę aplikacji, której to dotyczy.
  6. Ustal pierwotną przyczynęopartą na dowodach, a nie na prawdopodobnych domysłach.
  7. Przedstaw diagnozęprostym językiem, z widocznym tokiem rozumowania.
  8. Zastosuj poprawkędopiero po zatwierdzeniu diagnozy.
  9. Uruchom test ponownieaby potwierdzić poprawkę i wypatrywać nowych objawów.

To zmienia dynamikę pracy.

LLM nie „naprawia testu”. Odpowiada na pytania:

  • Czy to awaria asercji?
  • Czy selektor zniknął z komponentu?
  • Czy test wychwycił błąd JavaScript w runtime?
  • Czy to race condition?
  • Czy test używa złego waitera?
  • Czy bug jest w aplikacji, a nie w teście?

Skill regression-e2e: LLM jako strażnik standardów

Drugi skill skupiał się na pisaniu i rozszerzaniu testów regresyjnych.

Tutaj najważniejsze było egzekwowanie lokalnych konwencji:

  • każdy test regresyjny ma jasno opisany scenariusz,
  • każdy test ma adnotację z QaseID,
  • używamy Profilera do wychwytywania krytycznych błędów JavaScript,
  • polegamy na istniejących helperach,
  • selektory definiujemy przez data-testid,
  • przed kliknięciem asertujemy, że element jest widoczny,
  • nie dodajemy arbitralnych timeoutów,
  • nie tworzymy nowych helperów, jeśli odpowiedni już istnieje.

To brzmi jak dokumentacja. I dokładnie nią jest.

Różnica polega na tym, że ta dokumentacja nie leży sobie tylko w repo. Jest aktywnie używana przez LLM podczas pracy.

To była jedna z najważniejszych obserwacji z całego miesiąca:

Jeśli chcesz, żeby LLM dobrze pracował w projekcie, dobre prompty nie wystarczą. Potrzebujesz lokalnej pamięci projektu: reguł, helperów, antywzorców i przykładów.


Pliki wiedzy: pamięć zespołu dla LLM

Z czasem wyodrębniłem osobne pliki wiedzy dla E2E.

Jeden opisywał ogólne reguły testowania w Playwright. Drugi - konwencje specyficzne dla testów regresyjnych.

To tam mieszkały reguły takie jak te:

  • nie używaj waitForLoadState('networkidle'),
  • nie używaj magicznych sleepów przed asercjami,
  • używaj deterministycznych waitów,
  • sprawdzaj negatywną widoczność przez helper,
  • sprawdzaj opcjonalne elementy przez helper z timeoutem,
  • klikaj dopiero po toBeVisible(),
  • używaj typowanych selektorów i createLocators(),
  • testy, które dotykają płatności lub tworzenia konta, oznaczaj jako serial,
  • długie operacje muszą mieć uzasadniony timeout i komentarz.

Ważna uwaga: te reguły nie wzięły się z teorii. Wydestylowałem je z prawdziwych awarii.

Za każdą regułą stał konkretny incydent: timeout w CI, race condition, niestabilny modal, zewnętrzny sandbox, kruchy selektor albo fałszywie przechodząca negatywna asercja.


Przykład: networkidle wygląda dobrze, dopóki nie zaczyna szwankować

Jednym z klasycznych źródeł flakiness jest networkidle.

W teorii brzmi rozsądnie: poczekajmy, aż sieć się uspokoi. W prawdziwej aplikacji potrafi to być pułapka. Analytics, polling, keep-alive’y czy requesty wywoływane po renderze mogą sprawić, że test czeka zbyt długo albo kończy czekanie w złym momencie.

Źle:

await page.waitForLoadState('networkidle')

Dobrze:

await waitForAppToLoad(page)
await expect(page.getByTestId('nav.workspace.profile.email')).toBeVisible()

To mniej „magiczne” i bardziej powiązane z intencją testu.

Nie obchodzi mnie abstrakcyjny stan sieci. Obchodzi mnie to, czy aplikacja jest gotowa do interakcji i czy element, którego potrzebuję, jest widoczny.


Przykład: negatywne asercje mogą tworzyć fałszywą pewność

Inny częsty problem jest taki:

Źle:

await expect(locator).not.toBeVisible()

Ta asercja może przejść natychmiast, zanim element w ogóle zdąży się pojawić. W testach UI daje to fałszywy sygnał: test przechodzi, ale nie zweryfikował rzeczywistego zachowania.

Dobrze:

await expectNotVisible(ui.someElement)

Nie chodzi o to, że helper jest skomplikowany. Chodzi o to, że kod testu zaczyna wyrażać decyzję projektową: negatywna widoczność powinna poczekać, aż stan się ustabilizuje, a nie przechodzić przez przypadek.

Dla LLM też ma to znaczenie. Jeśli skill mówi „używaj expectNotVisible(), model nie wymyśla za każdym razem własnego wariantu.


Przykład: Profiler zamienia wywalający się test w lepszy sygnał

W testach E2E ścieżka użytkownika może przejść, podczas gdy aplikacja po drodze rzuca błędem JavaScript. Bez dodatkowego monitoringu taki test potrafi wyglądać na zielony.

Dlatego używamy w testach Profilera do zbierania krytycznych błędów przeglądarki.

Wzorzec jest prosty:

const profiler = new Profiler().start(page)

// logika testu

profiler.stop()
expect(profiler.criticalErrors.length).toEqual(0)

To pomaga wychwycić przypadki, w których UI „jakoś działa”, ale pod spodem pojawia się błąd runtime.

Dla workflow opartego na LLM też ma to znaczenie, bo skill /fix-e2e klasyfikuje takie błędy inaczej niż zwykłe awarie asercji. Najpierw musimy zrozumieć, czy test się wywalił, bo użytkownik nie widzi elementu, czy dlatego, że aplikacja wyprodukowała krytyczny błąd w tle.


Największa wartość: mniej decyzji podejmowanych od zera

Po kilku tygodniach zauważyłem, że największą korzyścią nie była sama generacja kodu.

Największą korzyścią było to, że wielu decyzji nie trzeba już było podejmować od zera.

Gdy pojawiał się wywalający się test, LLM miał procedurę. Gdy trzeba było dodać test regresyjny, miał szablon. Gdy pojawiał się opcjonalny modal, miał helper. Gdy test wymagał użytkownika z aktywnym planem, wiedział, którego helpera użyć. Gdy flow dotykało zewnętrznego sandboxa, wiedział, żeby nie odpalać go na ślepo równolegle.

To przyspiesza pracę, ale też ogranicza chaos.

W testach E2E chaos jest drogi. Dwa pozornie nieszkodliwe testy mogą sobie wzajemnie przeszkadzać. Jeden sleep potrafi dołożyć minuty do całego pakietu. Jeden niestabilny selektor potrafi podkopać zaufanie do całej regresji.


Co zadziałało najlepiej

Szczególnie dobrze zadziałały trzy rzeczy.

Po pierwsze: oddzielenie pisania testów od ich debugowania.

To różne tryby pracy. Pisząc test, myślisz o flow użytkownika, pokryciu regresją i czytelności. Debugując test, myślisz o objawach, źródle awarii i najmniejszej zmianie, która zachowuje intencję testu.

Po drugie: wymaganie diagnozy przed wprowadzeniem zmiany.

To ogranicza pokusę szybkiego łatania. LLM najpierw musiał wyjaśnić, co się stało, dlaczego się stało i jakie dowody za tym stoją.

Po trzecie: dokumentowanie antywzorców.

„Pisz dobre testy” nic nie znaczy. Konkretne reguły są dużo skuteczniejsze:

Antywzorce, których należy unikać
  • Nie używaj networkidle
  • Nie klikaj bez wcześniejszego asertowania widoczności
  • Nie dodawaj timeoutu bez powodu
  • Nie używaj isVisible() jako waitera
  • Nie osłabiaj asercji tylko po to, żeby test przeszedł

Czego LLM wciąż nie zastępuje

Ten workflow nie sprawił, że LLM „przejął testowanie”.

Wiedza o produkcie wciąż jest potrzebna. Wciąż trzeba rozumieć, czy wywalający się test wskazuje na buga w aplikacji, czy na buga w teście. Wciąż trzeba odróżnić flaky objaw od pierwotnej przyczyny. Wciąż trzeba wiedzieć, które asercje są krytyczne biznesowo, a które to tylko przypadkowe detale UI.

LLM jest bardzo dobry w pracy z kontekstem, porównywaniu wzorców, czytaniu błędów, proponowaniu zmian i podążaniu za checklistą.

Ale to człowiek wciąż musi ustalić standard.

Bez tego standardu LLM bardzo szybko potrafi wyprodukować mnóstwo testów, którym nikt nie ufa.


Najważniejszy wniosek

Po miesiącu pracy z testami E2E jestem przekonany, że najlepsze zastosowanie LLM w testowaniu to nie „napisz test”.

Najlepsze zastosowanie to zamiana wiedzy zespołu w wykonywalny workflow.

Skille, pliki wiedzy, katalogi helperów, reguły anty-flake’owe, szablony i procedury diagnostyczne sprawiają, że LLM zachowuje się jak narzędzie wbudowane w projekt, a nie jak zewnętrzny generator kodu.

I to właśnie różnica między AI-assisted coding a AI-assisted engineering.

W pierwszym przypadku dostajesz kod szybciej.

W drugim budujesz system, który pomaga utrzymać jakość w miarę, jak projekt rośnie, liczba testów się zwiększa, a CI musi pozostać godne zaufania.

Powiązane artykuły