Wszystkie wpisy

Self-healing testy AI - hype lokatora vs flake ze środowiska (luty 2024)Self-healing testy AI - hype lokatora vs flake ze środowiska (luty 2024)

English

Self-healing testy AI - hype lokatora vs flake ze środowiska (luty 2024)

Luty 2024: Testim, Mabl i Healenium są starsze niż hype. Playwright 1.41 nie ma oficjalnego self-healing GA. Healing nie leczy flake z CI.

AutomatyzacjaPlaywright

“Nie musisz już pisać stabilnych lokatorów”

Jest 15 lutego. Miesiąc temu, we wpisie o agentach, zapowiedziałem wprost, czego w tamtym tekście nie będzie: samonaprawiających się lokatorów, bo leczenie selektora to osobna kategoria narzędzi, starsza od tegorocznej fali agentów. Obiecałem wrócić do niej osobno i wracam, bo od tamtego wpisu w czterech rozmowach o czerwonym E2E usłyszałem to samo zdanie: nie musisz już pisać stabilnych lokatorów, AI podmieni selektor, kiedy strona się zmieni.

Zdanie jest kuszące i nie dlatego, że ktoś je zręcznie napisał. Jest kuszące, bo dotyka prawdziwego bólu. Kruchy lokator to nie jest wymyślony problem: wystarczy jeden restyling, w którym szablon dostaje nowe nazwy klas, żeby w nocnym przebiegu zrobiło się czerwono w dziesięciu plikach naraz, i żeby ranek zeszedł na mechanicznym przepisywaniu selektorów bez jednej myśli o produkcie. Każdy, kto to przeżył, chce przycisku, po którym to się robi samo.

Moja teza na luty 2024 jest wąska i będę jej bronił przez cały wpis. Self-healing to stary produkt SaaS i OSS, sprzedawany na nowo jako AI, a nie nowy przycisk w Playwrighcie. Leczenie lokatora leczy jedną klasę awarii i nie dotyka flake’a ze środowiska, z danych ani z wyścigu. Nie zdejmuje też recenzji, którą w lipcu zostawiłem przy hipotezach z logu; przesuwa ją tylko w miejsce, gdzie trudniej ją wykonać, bo podmiana dzieje się w runtime, a nie w moim commicie.

Stan warsztatu na dziś, żeby za rok było wiadomo, o czym mówię. Runner to Playwright 1.41.1 z 19 stycznia. Oficjalnego self-healingu w tej wersji nie ma i w sekcji o Playwrighcie wracam do tego z notatką wydania w ręku. Copilot Chat mam od 29 grudnia w stabilnym VS Code i do leczenia lokatorów nie nadaje się z prozaicznego powodu: widzi otwarty plik, nie widzi DOM z nocnego przebiegu. Modele to nadal GPT-4 i GPT-4 Turbo z listopadowego DevDay. Dzisiaj, 15 lutego, Google ogłosiło Gemini 1.5 Pro w ograniczonym podglądzie i odnotowuję tę datę wyłącznie dlatego, że wypada tego samego dnia co mój slot, bo nie zrobiłem na tym modelu ani jednej rzeczy opisanej poniżej.

Czego tu nie będzie. Nie będzie zdania, że Playwright dostał uzdrowiciela w rdzeniu, bo nie dostał. Nie będzie tutoriala z Healenium, Testim ani Mabl jako nowości lutego, bo te trzy produkty są starsze od słowa, którym się je dziś sprzedaje. Nie będzie healera podpiętego do mojego CI. Będzie jedno syntetyczne demo logowania w moim własnym repozytorium z przykładami do bloga, trzydzieści linii heurystyki, którą napisałem sam, i dwie czerwienie: jedną ta heurystyka naprawia, drugą zakrywa.

Trzy produkty starsze od hype’u

Fala, o której dziś mówi się jak o nowości 2024, ma daty. Wypisuję je, bo w rozmowach wszystkie trzy narzędzia lądują w jednym worku z “nowym AI”, a żadne z nich nie jest w tym worku nowe.

Testim sprzedawał Smart Locators na długo przed tegoroczną falą. “AI locators” stoją w komunikacie o przejęciu firmy przez Tricentis z 9 lutego 2022, czyli dwa lata i sześć dni przed tym wpisem. To nie jest więc produkt młodszy od ChatGPT. To produkt, który w dniu premiery ChatGPT miał już swoje miejsce w cenniku większego dostawcy.

Mabl istnieje od 2017, a jego auto-healing jest opisany w blogu produktu, nie w changelogu z lutego 2024. Mechanizm nazwano tam dokładnie tak, jak działa: narzędzie zapamiętuje wiele identyfikatorów tego samego elementu i kiedy jeden z nich przestaje trafiać, adaptuje krok do tego, który trafia dalej.

Healenium to ten sam pomysł w open source i w kształcie Selenium. Jest proxy i wrapperem: przechwytuje findElement, a kiedy dostaje NoSuchElement, szuka w zapisanym drzewie najbardziej podobnego węzła z poprzedniego zdrowego przebiegu i podstawia go w miejsce lokatora, który padł. Hub na EPAM Solutions Hub ma linię 3.x od 9 października 2020, kiedy wyszło 3.0.1, a 3.1 jest z 19 maja 2021; repozytorium w organizacji na GitHubie powstało 7 października 2021. Patcha z lutego 2024 nie pinuję, bo nie mam adresu, który by go na dziś potwierdzał, a daty, które mam, wystarczają do postawienia tezy.

Teza jest prosta: żaden z tych trzech produktów nie wyszedł w 2024. Wyszedł hype, w którym heurystyka na drzewie DOM dostała nowe słowo i wjechała do tej samej rozmowy co styczniowi agenci.

Warto ten mechanizm nazwać, bo kiedy się go nazwie, “AI” robi się w nim zdumiewająco mało. Wszystkie trzy narzędzia robią trzy rzeczy w tej samej kolejności. Zapamiętują więcej niż jeden sposób znalezienia elementu, zwykle przy zielonym przebiegu: identyfikator, tekst, rolę, atrybuty, pozycję w drzewie, sąsiedztwo. Wykrywają pudło, czyli brak trafienia albo wyjątek. Wybierają najbardziej podobnego kandydata z tego, co na stronie zostało, według własnej funkcji podobieństwa. To jest porównywanie drzew z wagami, a nie rozumowanie o produkcie, i działało to u nich wszystkich, zanim ktokolwiek sprzedawał QA przez model językowy.

Jest przy tym powód, dla którego ta obietnica wraca co kilka lat w nowym opakowaniu, i nie ma w nim złej woli. Dostawca SaaS do testów nie sprzedaje asercji, tylko utrzymanie zestawu, a “twoje testy naprawiają się same” to najlepsze zdanie, jakie da się napisać na stronie produktu rozliczanego za utrzymanie. Healenium sprzedaje to samo zdanie bez faktury. Zwracam tylko uwagę, że zdanie o samonaprawie nie mówi niczego o tym, co się dzieje z sygnałem.

Różnica kategorii wobec stycznia jest przy tym ostra i chcę ją zapisać, bo oba tematy zlewają się w rozmowach. Agent dostaje cel i sam dobiera narzędzia; healer dostaje padający lokator i dobiera inny, który trafia. Pętla z mojego styczniowego demo zmyśliła data-testid, którego w formularzu nie było. Healer popełnia błąd odwrotny i kończy w tym samym miejscu: znajduje coś, co na stronie naprawdę jest, i uznaje to za równoważne temu, czego szukałem. Oba potrafią dać zielony wynik bez wiedzy, dlaczego było czerwono, i oba zasługują na recenzję jak diff, a nie na zaufanie jak asercja.

Co healer leczy, a czego nie tknie

Healing leczy jedną klasę awarii i trzeba jej oddać sprawiedliwość, bo jest częsta i nudna. Zmienił się id, klasa albo ścieżka w drzewie, element nadal jest na stronie, nadal ma tę samą rolę i nadal robi to samo. Przy restylingu taka awaria wywraca dziesiątki testów naraz i nikt przy zdrowych zmysłach nie nazwie jej błędem produktu.

Z tą klasą walczę od sierpnia 2021 i warto zauważyć, że healer rozwiązuje ją z przeciwnej strony niż ja. Wniosek z tamtego refaktoru brzmiał: lokator jest opisem, jak znaleźć element, rozwiązywanym w momencie akcji, a nie uchwytem zapamiętanym wcześniej. Dlatego wyleciały wtedy PageFactory i [CacheLookup]. Healer bierze ten sam problem i mówi: zostaw kruchy opis, ja podmienię go w locie. Jeden kierunek sprawia, że opis znosi zmianę w drzewie. Drugi sprawia, że zmiana w drzewie przestaje być widoczna.

Mam w tej sprawie własny dług i nie będę udawał, że go nie ma. W pierwszym projekcie Playwrighta z maja 2022 pisałem page.locator('.new-todo'), czyli lokator po klasie CSS, więc dokładnie ten rodzaj zapisu, który restyling rozbija. Poprawiam to od tamtej pory rolą i atrybutem dedykowanym testom, i jest to robota na kilka godzin w jednym pliku, a nie funkcja do kupienia.

A teraz rzeczy, których healing nie tknie, bo nie są lokatorem.

Nie leczy timeoutu sieci. Kiedy API logowania nie wraca w piętnastu sekundach, w drzewie nie ma nic do podmiany, bo drzewo jest w porządku. Nie leczy współdzielonych danych i to jest moja ulubiona kontra, bo mam ją odtworzoną: flake, który rozpisałem w lipcu, siedział w kodzie rabatowym z licznikiem użyć, zużywanym przez kolejne nocne przebiegi na trwałym środowisku. Żadna podmiana selektora go nie dotyka, bo przycisk był tam, gdzie zawsze, tylko wyłączony przez walidację po stronie serwera. Reguła z porządkowania xUnit i NUnit brzmi w tej sprawie tak samo jak w styczniu 2022: najpierw ustal, gdzie żyje stan, potem dobieraj narzędzia.

Nie leczy wyścigu. Kiedy przycisk jest w DOM, ale jeszcze disabled, bo hydracja nie skończyła się przed kliknięciem, healer znajdzie element bez trudu i klik nadal nic nie zrobi. Nie leczy też różnic na agencie CI. Font, viewport i plasterek środowiska, którego nie ma na moim laptopie, to kwestia renderu i izolacji, czyli temat, który rozpisałem w równoległych testach na CI i w Testcontainers na Azure Pipelines. Flake ze środowiska zostaje flakiem ze środowiska, a jedyna rzecz, która go rusza, to własna zależność podniesiona na czas joba.

Koszyk awarii Czy to lokator Co zrobi healer
Zmieniony id albo klasa, element nadal na stronie tak podmienia i daje zielone
Timeout API na środowisku nocnym nie nie ma czego podmienić albo trafia w inny element
Współdzielony fixture danych, wyczerpany licznik nie nic, awaria nie siedzi w drzewie
Wyścig i hydracja, przycisk disabled nie element znajduje, klik dalej nic nie robi
Font i viewport na agencie CI nie nic, różnica jest w renderze

Jeden wiersz na pięć. Z wrześniowego podziału po triggerze wynika przy tym, gdzie te wiersze mnie bolą: bramka bierze jednostki, kontrakty i jeden smoke, więc cała ta tabela, poza tym jednym smoke’em, rozgrywa się w nocnym przebiegu, i to tam healer miałby pracować bez nadzoru. Pixel z listopada 2022 nie pomaga mi ich rozdzielić: screenshot mówi, że zmienił się wygląd, healer mówi, że zmienił się selektor, a żadne z nich nie mówi, czy zmienił się produkt.

Pokażę to na jednym syntetycznym teście logowania. Repozytorium jest moje, z przykładami do bloga, aplikacja-fixture wstaje obok tym samym plikiem compose, a dane są wymyślone.

test('logowanie wpuszcza użytkownika z poprawnym hasłem', async ({ page }) => {
  await page.goto('/login')
  await page.locator('#email').fill('test@test.com')
  await page.locator('#password').fill('test')
  await page.locator('#login-submit').click()
  await expect(page.getByRole('heading', { name: 'Pulpit' })).toBeVisible()
})

Wariant A to zmiana identyfikatora. Przycisk dostaje id="submit-login" zamiast id="login-submit", tekst zostaje “Zaloguj”, rola zostaje button. Test pada na lokatorze:

  1) [chromium] › auth/login.spec.ts:12:3 › logowanie wpuszcza użytkownika z poprawnym hasłem

    TimeoutError: locator.click: Timeout 15000ms exceeded.
    =========================== logs ===========================
    waiting for locator('#login-submit')
    ============================================================

Healera w Playwrighcie nie mam, więc napisałem własną heurystykę, żeby zobaczyć, jak wygląda raport po podmianie. Trzydzieści linii, zero modelu, jedna rola i jedna nazwa jako zapasowy sposób znalezienia elementu:

// helpers/healing.ts - moja własna heurystyka, nie funkcja Playwrighta
import { expect, type Page } from '@playwright/test'

type Hint = { role: 'button' | 'link'; name: string }

export async function healingClick(page: Page, selector: string, hint: Hint) {
  const primary = page.locator(selector)
  if ((await primary.count()) > 0) {
    await primary.click()
    return
  }
  const healed = page.getByRole(hint.role, { name: hint.name })
  console.log(`[heal] ${selector} -> role=${hint.role} name="${hint.name}"`)
  await healed.click()
}

export async function healingVisible(page: Page, role: 'heading', name: string) {
  const exact = page.getByRole(role, { name })
  if ((await exact.count()) > 0) {
    await expect(exact).toBeVisible()
    return
  }
  const relaxed = page.getByRole(role).first()
  console.log(`[heal] ${role}:"${name}" -> ${role}:"${await relaxed.innerText()}"`)
  await expect(relaxed).toBeVisible()
}

Po zamianie click na healingClick wariant A przechodzi. Element został znaleziony po roli i nazwie, test jest zielony, a cały ślad po tej operacji to jedna linia, którą sam wypisałem na standardowe wyjście:

[heal] #login-submit -> role=button name="Zaloguj"
  1 passed (3.8s)

I to jest jedyny przypadek, w którym mechanizm robi dokładnie to, co obiecuje. Rachunek jest jednak od razu widoczny: w raporcie nocnego przebiegu nie ma informacji, że produkt zmienił identyfikator przycisku logowania. Jeśli zmiana była przypadkiem, nikt o niej nie usłyszy. Jeśli była celowa i łamie kontrakt automatyzacji, kontrakt został właśnie cicho przepisany, w nocy, bez commita.

Wariant B to ta sama czerwień o zupełnie innej przyczynie. Identyfikator jest bez zmian, za to /auth na środowisku nocnym nie odpowiada. Przycisk jest w DOM, klik się wykonuje, logowanie się nie kończy, więc pada asercja o pulpicie:

    TimeoutError: expect(locator).toBeVisible() failed

    Locator: getByRole('heading', { name: 'Pulpit' })
    Expected: visible
    Timeout:  5000ms

Tutaj healingClick nie ma nic do roboty, bo przycisk jest na miejscu. Ale healing w kształcie Healenium nie siedzi na kliknięciu, tylko na każdym szukaniu elementu, więc rozciągnąłem heurystykę na asercję i puściłem jeszcze raz:

[heal] heading:"Pulpit" -> heading:"Zaloguj się"
  1 passed (4.1s)

Test, który miał mi powiedzieć, czy logowanie działa, powiedział mi, że strona logowania ma nagłówek. Zielone, poniżej pięciu sekund, bez jednego czerwonego znaku w raporcie. Wariant B jest flakiem ze środowiska i podmiana lokatora jest tu błędem kategorii, dokładnie tym samym, który w styczniu popełniła pętla, kiedy zamiast sprawdzić flagę w konfiguracji dopisała do testu atrybut, którego nie było.

Obie czerwienie przepuściłem dwadzieścia razy pod rząd, raz z heurystyką i raz bez niej:

Wariant Bez heurystyki Z heurystyką Czy raport mówi, co się zmieniło
A: id przycisku login-submit na submit-login 20 na 20 czerwone 20 na 20 zielone nie, poza moim console.log
B: /auth bez odpowiedzi, id bez zmian 20 na 20 czerwone 20 na 20 zielone nie, a werdykt jest fałszywy

Dwa wiersze, jedna heurystyka, dwie różne prawdy i jeden raport, który ich nie rozdziela. Z tego ćwiczenia wyciągam wniosek, którego się nie spodziewałem: trudna część self-healingu nigdy nie siedziała w znalezieniu zapasowego lokatora. To mi zajęło trzydzieści linii i jeden wieczór. Trudna część siedzi w decyzji, czy podmiana jest dozwolona, i tej decyzji nie ma ani w moim helperze, ani w cenniku SaaS, ani w drzewie zapisanym przez Healenium.

Playwright 1.41.1 nie ma uzdrowiciela

Playwright czeka na element, retryuje asercję i zapisuje trace. To trzy dobre rzeczy i żadna z nich nie jest self-healingiem.

Auto-waiting czeka na ten sam lokator, aż element będzie widoczny, aktywny i stabilny. Healing podstawia inny lokator. Pierwsze jest cierpliwością, drugie podmianą, i to nie jest różnica stopnia, tylko rodzaju. Retry asercji działa identycznie: przez cały timeout sprawdza ten sam warunek, a nie szuka warunku, który się uda.

Przeczytałem notatkę wydania tagu v1.41.1 i nie znalazłem w niej ani self-healingu, ani agenta leczącego lokatory. Nie będę pinował funkcji, której nie ma w notatce wydania, bo właśnie z takich zdań robią się potem mity. Jeżeli ktoś sprzedaje “AI locators for Playwright” jako wtyczkę albo usługę, to jest osobny produkt, z osobną recenzją i osobnym miejscem w umowie, a nie funkcja runnera, który mam w package.json.

To, co faktycznie robię, gdy lokator pada, nie zmieniło się od czasu, kiedy w ogóle mam trace. Otwieram trace i czytam, czy element był w drzewie, czy był niewidoczny, czy był przykryty i co w tym czasie robiła sieć. W wariancie A migawka DOM pokazuje przycisk z nowym identyfikatorem i to jest dwusekundowy werdykt. W wariancie B ta sama migawka pokazuje przycisk na swoim miejscu, a zakładka z żądaniami pokazuje wiszące /auth, i to jest werdykt o środowisku, nie o teście. Dwa kliknięcia, dwie różne odpowiedzi, i trace o żadnej z nich nie mówi “uzdrowione”, bo nie ma takiego pojęcia.

Jest też wariant, w którym heurystyka podobieństwa jest naprawdę przydatna, i nie chcę udawać, że go nie widzę. Masowa zmiana identyfikatorów po restylingu, trzydzieści padających testów, wszystkie z tej samej przyczyny. Wtedy chcę listy propozycji w pull requeście, uruchomionej przeze mnie na żądanie, z diffem na każdy plik i z moim podpisem pod całością. To jest codemod, nie uzdrowiciel. Różnica jest w tym, kto i kiedy decyduje: raz, świadomie, w repozytorium, a nie trzydzieści razy, w nocy, w runtime.

Nad każdym czerwonym testem zadaję trzy pytania i żadne z nich nie brzmi “czy można to zahealować”. Czy ten test w ogóle coś sprawdza, czy tylko czeka na element. Czy zmiana jest w produkcie, czy w środowisku. Czy potrafię odtworzyć awarię, zanim zaproponuję poprawkę. To ten sam zestaw, który zostawiłem w styczniu przy pętli agenta i w lipcu przy hipotezach z logu. Healing odpowiada na pytanie czwarte, którego nie zadałem: czy istnieje inny selektor, przy którym ten krok przejdzie. Przy refaktorze to pytanie jest przydatne. Jako automat w nocnym przebiegu jest szkodliwe.

Gdy healer i tak wejdzie do zespołu

Nie zakażę tego narzędzia w cudzym zespole i nie mam takiego zamiaru, bo przy masowej zmianie identyfikatorów rozumiem, skąd bierze się na nie zapotrzebowanie. Mogę powiedzieć, pod jakimi warunkami nie skasuje sygnału, i mam takie cztery.

Pierwszy: podmiana jest zdarzeniem w raporcie, nie w logu, który czyta jedna osoba. Stary lokator, nowy lokator, zrzut elementu, w który nowy trafił, i nazwa testu, który przez to przeszedł. Recenzuję to jak diff: najpierw odtwarzam, potem zgadzam się albo skreślam. W moim demo tę rolę grała jedna linia na standardowym wyjściu i wystarczyło zgubić ją w czterdziestu zielonych testach, żeby cała historia przestała istnieć.

Drugi: podmiana wygasa. Heal, który przeżywa pięć nocnych przebiegów, nie jest już healem, tylko cichym przepisaniem testu. Po wygaśnięciu chcę czerwonego, czyli dokładnie tego, co healer miał usunąć, bo tylko czerwone wymusza decyzję, czy nowy lokator ma trafić do repozytorium.

Trzeci: healing nie wchodzi na bramkę i nie wchodzi na asercje. Na bramce po wrześniowym podziale stoją rzeczy szybkie i deterministyczne, a mechanizm, który na tym samym wejściu raz przechodzi z lokatora z pliku, a raz z lokatora dobranego przez siebie, deterministyczny nie jest. Asercje odpadają z powodu wariantu B: moment, w którym “uzdrowieniu” podlega cel asercji, jest momentem, w którym test przestaje cokolwiek twierdzić.

Czwarty: ma właściciela. W podsumowaniu 2023 zapisałem cztery rzeczy, po których poznaję, że narzędzie weszło do zespołu, a nie do hype’u: nazwane zadanie, miejsce w procesie, wiersz w pipelinie albo świadomy powód, dla którego go nie ma, i człowiek odpowiadający za wynik. Healer ma pierwsze, jeśli ograniczymy go do “zaproponuj inny lokator przy refaktorze”. Nie ma czwartego, jeśli w nocy sam przepisuje zestaw.

Kontrast z lipcem jest tu obowiązkowy, bo te dwa tematy mylą się najczęściej. W triage flake’ów model dostawał fragment logu i oddawał trzy hipotezy. Z trzydziestu trzech hipotez dziewięć potwierdziłem odtworzeniem, szesnaście odrzuciłem, a ta postawiona jako najbardziej prawdopodobna w moim głównym przykładzie okazała się nieprawdziwa. Kluczowe jest jedno: żadna z nich nie zmieniła ani jednej linii testu bez mojego commita. Self-healing działa w drugą stronę. Zmienia lokator w runtime, a log, jeśli w ogóle powstaje, opisuje sukces, nie przyczynę. To nie jest triage. To automatyczna poprawka bez ownera, i przy czymś takim mój lipcowy notes byłby niewykonalny, bo nie miałbym czego odtwarzać.

Podsumowanie

Self-healing jest tematem tego miesiąca, bo słowo “AI” przykleiło się do produktów, które znam od lat. W lutym 2024 nie kupuję go jako zamiennika izolacji i recenzji.

Trzy zdania, które chcę móc przeczytać za rok. Testim reklamował Smart Locators już w komunikacie o przejęciu z 9 lutego 2022, Mabl istnieje od 2017, a linia 3.x Healenium ma pierwsze znane mi wydanie z 9 października 2020, więc tegoroczny hype dotyczy mechanizmu, nie premiery. Playwright 1.41.1 nie ma oficjalnego self-healingu, a auto-waiting nim nie jest, bo czeka na ten sam lokator i nie dobiera innego. Trzydzieści linii mojej własnej heurystyki naprawiło zmianę id w dwudziestu przebiegach na dwadzieścia i w tych samych dwudziestu zakryło timeout /auth fałszywym zielonym, więc koszt nie był w jakości mechanizmu, a w tym, że raport stracił zdanie o tym, czy zmienił się produkt.

Czego z tego nie wynika. Nie wynika, że Smart Locators są bez wartości, bo przy restylingu, który przepisuje pół szablonu, lista propozycji w pull requeście oszczędza realne godziny. Nie wynika, że nigdy nie wstawię heurystyki “znajdź ten sam przycisk po roli i nazwie” do helpera, bo właśnie ją napisałem i zostawiam w repozytorium przykładów jako narzędzie do uruchamiania ręcznie. Nie wynika też, że to się nie zmieni, jeśli uzdrowiciel trafi kiedyś do rdzenia runnera; dziś go tam nie ma, więc nie mam o czym pisać poza datą.

Warunek, po którym poznam, że temat dojrzał, formułuję tak: podmiana lokatora jest akceptowalna wtedy, kiedy zostawia audytowalny diff, wygasa po kilku przebiegach, nigdy nie dotyka celu asercji i nie zdejmuje z człowieka pytania, czy czerwony test złapał produkt, czy środowisko. Do tego czasu mój zestaw nie ma autonomicznego uzdrowiciela. Ma izolację, trace i mnie.