Obietnica z sierpnia
W sierpniu, opisując pipeline z kontenerami i Playwrightem, wypisałem listę rzeczy, których świadomie do joba e2e nie dokładam. Porównywanie obrazków było jedną z nich. Napisałem wtedy, że screenshoty na CI to rozmowa o progach tolerancji, o różnicach w renderowaniu między maszynami i o tym, kto zatwierdza nowe wzorce, więc dorzucenie ich do gotowego pipeline’u bez tych ustaleń da mi czerwone przebiegi bez informacji. Podtrzymuję to zdanie. Dzisiaj chcę tylko wreszcie odbyć tę rozmowę.
Wizualną regresję opisywałem na blogu dokładnie raz, w marcu 2020, darmową wtyczką do Cypressa. Pytanie brzmiało wtedy wąsko: czy da się porównywać zrzuty ekranu w narzędziu, które już mam, i nie zapłacić za to ani złotówki. Odpowiedź była twierdząca, plugin złapał usuniętą kropkę w zdaniu “No articles are here… yet.” i tamten wpis kończył się jednym otwartym wątkiem, czyli CI.
Dwa i pół roku później pytanie jest inne, bo zmieniło się otoczenie. Playwright ma asercję na screenshotach w rdzeniu runnera od maja. BackstopJS, który jest starszy od całej mojej serii o Cypressie, od zeszłego listopada umie chodzić na silniku Playwrighta. Percy i Applitools mają SDK pod Playwrighta, jedno od października zeszłego roku, drugie od sierpnia 2020. Zamiast jednej wtyczki mam więc cztery drogi plus piątą, historyczną. Ten wpis jest o wyborze między nimi.
Od razu granica tekstu: to nie jest tutorial. Nie przepisuję wpisu z 2020 na Playwrighta i nie buduję pełnego zestawu wizualnego na Conduit. Listingi poniżej są krótkie i mają pokazać kształt API, a nie zastąpić dokumentacji. Interesuje mnie macierz kompromisów, bo to ona decyduje, czy porównywanie obrazków po trzech miesiącach nadal komuś służy, czy jest wyłączone flagą.
Trzy modele, nie pięć narzędzi
Zanim wypiszę paczki, wypiszę oś, na której one leżą. Narzędzia różnią się API, ale realny wybór jest między trzema modelami i to on kosztuje.
Wzorzec w repozytorium. Plik PNG leży obok testu, git go wersjonuje, a zatwierdzenie nowego wyglądu jest commitem. Tak działa Playwright w rdzeniu, tak działa Backstop, tak działała moja wtyczka z 2020 i tak działa OSS-owy lost-pixel. Licencja kosztuje zero, ale płacę trzema innymi rzeczami: repozytorium puchnie od binariów, review zmiany wyglądu wygląda jak review pliku binarnego, a wynik zależy od maszyny, która renderowała.
Wzorzec w usłudze. Zrzuty jadą do serwisu, diff i akceptacja dzieją się w webowym UI, a wzorzec jest przypięty do gałęzi po stronie dostawcy. Tak działa Percy. Płacę pieniędzmi i tym, że obrazy mojej aplikacji wychodzą na cudzy serwer, a w zamian dostaję rzecz, której git nie daje: człowiek nietechniczny może kliknąć “approve”.
Porównanie, które nie jest per piksel. Applitools sprzedaje Eyes właśnie jako algorytmy porównujące, z trybami, które potrafią zignorować przesunięcie treści albo zmianę danych, a złapać zmianę układu. To jest algorytm zaszyty w produkcie i uruchamiany po ich stronie, a nie narzędzie, które samo rozstrzyga, czy zmiana jest pożądana. Rozróżnienie jest ważne, bo marketingowe “AI” w tej kategorii oznacza konkretny, deterministyczny sposób liczenia różnicy, i tak go tutaj traktuję.
Kryterium, które przy tym podziale waży najwięcej, to nie dokładność, tylko akceptacja. Każde z tych narzędzi kiedyś zaświeci na czerwono z powodu zmiany, która jest poprawna. Pytanie brzmi, ile kosztuje przepchnięcie takiej zmiany dalej. W modelu z gitem to nadpisanie wzorca i commit, w modelu z usługą to kliknięcie w przeglądarce. Kiedy koszt akceptacji jest wysoki, ludzie zaczynają ignorować wynik i wracamy do problemu, który opisywałem we wrześniu przy strategii testów: bramka, której nikt nie czyta, nie jest bramką.
Playwright 1.27: asercja w rdzeniu
Zaczynam od najtańszej opcji, bo mam ją już zainstalowaną. @playwright/test stoi u mnie na 1.27.1 z 12 października i to jest jedyna wersja, na której cokolwiek tutaj sprawdzałem.
Asercja toHaveScreenshot() jest w narzędziu od 1.22.0 z 12 maja, czyli od dokładnie tego wydania, na którym zakładałem pusty projekt i o którym napisałem wtedy jedno zdanie, że wizualna regresja to osobny temat. Nie jest to więc świeżynka listopada, tylko funkcja, która ma pół roku i przez ten czas zdążyła się uspokoić.
import { test, expect } from '@playwright/test'
test('ekran logowania zgadza się z wzorcem', async ({ page }) => {
await page.goto('/login')
await expect(page).toHaveScreenshot('login.png', {
maxDiffPixelRatio: 0.01,
})
})Trzy rzeczy w tym listingu robią różnicę wobec tego, co miałem w 2020 roku. Po pierwsze, asercja jest web-first: runner robi zrzut, czeka, robi kolejny i porównuje je ze sobą, aż strona przestanie się ruszać, dopiero potem zestawia wynik ze wzorcem. Po drugie, animacje i kursor są przy tym domyślnie wyłączane, więc nie muszę zabijać CSS-owych przejść własnym stylem. Po trzecie, próg podaję jawnie, w maxDiffPixelRatio albo w maxDiffPixels, i to jest liczba, którą warto ustalić raz dla projektu, a nie dobierać po każdym czerwonym przebiegu.
Wzorce lądują obok pliku testowego, w katalogu z sufiksem -snapshots, a nazwa pliku zawiera projekt i system, na przykład login-chromium-linux.png. Aktualizacja to jedna flaga:
$ npx playwright test --update-snapshotsKonsekwencja tego nazewnictwa jest ważniejsza, niż wygląda: wzorzec jest związany z platformą. Zrzut wygenerowany na moim Linuksie nie zgodzi się ze zrzutem z macOS kolegi, bo różni się wygładzanie czcionek, a czasem same czcionki. Jedyne sensowne wyjście to generowanie i weryfikowanie wzorców w tym samym obrazie kontenera, w którym chodzi CI, czyli dokładnie w tym, co ustawiałem przy Testcontainers na agencie i w sierpniowym pipelinie. Kto tego nie zrobi, dostanie narzędzie, które czerwieni się przy każdej zmianie maszyny.
Starsze API nie znika. page.screenshot() nadal służy do artefaktów, a expect(buffer).toMatchSnapshot() dalej istnieje i jest ogólniejsze, bo porównuje dowolny bufor, nie tylko obraz strony. Do wizualnej regresji biorę jednak toHaveScreenshot(), właśnie za czekanie na stabilizację, którego w tamtym wariancie muszę pilnować sam.
BackstopJS 6.1.4
Backstop jest w npm od 2015 roku i to jest jego pierwsza zaleta: nie jest eksperymentem. Wersja z 4 listopada to 6.1.4, a od 6.0.1 z 18 listopada zeszłego roku narzędzie potrafi używać Playwrighta jako silnika zamiast Puppeteera.
Różnica wobec poprzedniej sekcji jest zasadnicza i nie polega na jakości diffa. Backstop to osobny runner z własnym plikiem konfiguracyjnym, w którym opisuję listę adresów i siatkę rozdzielczości. Nie piszę testów, tylko scenariusze:
{
"id": "conduit",
"engine": "playwright",
"viewports": [
{ "label": "phone", "width": 375, "height": 667 },
{ "label": "desktop", "width": 1366, "height": 768 }
],
"scenarios": [
{
"label": "logowanie",
"url": "http://localhost:4100/login",
"misMatchThreshold": 0.1
},
{
"label": "lista artykulow",
"url": "http://localhost:4100/",
"misMatchThreshold": 0.1
}
],
"paths": {
"bitmaps_reference": "backstop_data/bitmaps_reference"
}
}$ npx backstop reference
$ npx backstop test
$ npx backstop approveZa to płacę tym, że mam w projekcie drugi runner. Konfiguracja playwright.config.ts nie obowiązuje w backstop.json, baseURL trzeba podać drugi raz, a logowanie czy przygotowanie danych wymaga własnych skryptów onBefore i onReady, więc cała robota, którą mam już zrobioną w page objectach i w helperach logowania, jest tam nie do użycia bez przepisania.
Dlatego widzę dla Backstopa jedno konkretne miejsce: kiedy chcę siatkę wielu adresów na kilku szerokościach ekranu i nie chcę do tego pisać testów. Strona marketingowa, biblioteka stylów, zestaw landing page’y. Do aplikacji, do której trzeba się zalogować i przeklikać trzy kroki, wolę zostać w runnerze, w którym te trzy kroki już opisałem. Raport HTML z trzema panelami, wzorzec, aktualny stan i diff, jest przy tym najczytelniejszy z całej stawki OSS.
Percy
@percy/playwright wyszło w 1.0.0 20 października zeszłego roku, a od 1.0.4 z 24 maja nie doczekało się nowszego stabilnego wydania, więc to jest wersja, którą dziś przypinam. Kod w teście jest krótszy niż wszystko powyżej:
import { test } from '@playwright/test'
import percySnapshot from '@percy/playwright'
test('ekran logowania', async ({ page }) => {
await page.goto('/login')
await percySnapshot(page, 'Logowanie')
})$ PERCY_TOKEN=... npx percy exec -- npx playwright testW teście nie ma progu, nie ma nazwy pliku i nie ma asercji. To nie przeoczenie w listingu, tylko cały model: percySnapshot zbiera stan strony i wysyła go do usługi, a porównanie i decyzja dzieją się poza przebiegiem. Przebieg z samymi snapshotami nie ma prawa być czerwony z powodu wizualnego, bo w tym momencie nikt jeszcze niczego nie porównał. Wynik wraca jako status na pull requeście po stronie GitHuba i to tam ktoś klika akceptację.
Plusy są dwa i oba są organizacyjne, nie techniczne. Wzorzec nie jest plikiem w moim repozytorium, więc nie muszę rozstrzygać, kto commituje binaria i co zrobić z konfliktem na PNG. Akceptację może kliknąć osoba, która nie ma repozytorium na dysku, czyli projektant albo product owner, a to jest jedyny znany mi sposób, żeby zmiana wyglądu była zatwierdzana przez kogoś, kto o wyglądzie decyduje.
Minusy też są dwa. Pierwszy: obrazy mojej aplikacji wychodzą na zewnątrz i to jest rozmowa, którą trzeba odbyć przed pilotem, a nie po. Drugi: to jest usługa rozliczana za snapshoty, więc liczba zrzutów razy liczba szerokości ekranu razy liczba przebiegów to jest budżet, a nie szczegół konfiguracji. Nie podaję tu żadnych kwot, bo cennik żyje własnym życiem, a wpis zostaje w internecie na lata. Podaję za to zasadę: policz swoje snapshoty przed włączeniem tego w nocnym harmonogramie, bo mnożnik z macierzy potrafi zaskoczyć.
Applitools Eyes
@applitools/eyes-playwright istnieje od 1.0.0 z 5 sierpnia 2020, a wersja na dziś to 1.12.7 z 11 października. API jest bardziej rozbudowane niż w Percy, bo to nie jest jedno wywołanie, tylko cykl życia sesji:
import { test } from '@playwright/test'
import { Eyes, Target, VisualGridRunner } from '@applitools/eyes-playwright'
const runner = new VisualGridRunner()
test('ekran logowania', async ({ page }) => {
const eyes = new Eyes(runner)
await eyes.open(page, 'Conduit', 'ekran logowania')
await page.goto('/login')
await eyes.check('formularz', Target.window().fully())
await eyes.close()
})Dwie rzeczy odróżniają to od reszty. Pierwsza to Ultrafast Grid, czyli VisualGridRunner w listingu: przeglądarka lokalna robi jedno przechwycenie strony, a renderowanie w wielu przeglądarkach i rozdzielczościach dzieje się po stronie usługi. Zamiast puszczać własną macierz z wpisu o cross-browser i mnożyć czas przebiegu przez liczbę kombinacji, wysyłam jeden zestaw danych i dostaję wynik dla wielu konfiguracji. Przy dziesięciu kombinacjach to jest realna różnica w wall-clocku.
Druga to tryby dopasowania. Domyślny nie jest zwykłym porównaniem piksel po pikselu, a tryb Layout celowo ignoruje treść i patrzy na rozkład elementów. To jest odpowiedź na problem, który w podejściu z progiem tolerancji rozwiązuje się źle: strona z dynamiczną datą, licznikiem albo losową kolejnością rekordów będzie czerwona za każdym razem, a podniesienie progu na tyle, żeby to przepuścić, przepuści też realną usterkę. Nie mam z tym mechanizmem przerobionych miesięcy i nie będę udawał, że mam. Zapisuję jako rzecz do sprawdzenia na aplikacji z dużą ilością zmiennych danych, bo to jedyne miejsce, gdzie ma szansę wygrać z prostym diffem.
Koszt jest tu tej samej natury co przy Percy, z tą różnicą, że model licencyjny jest bardziej korporacyjny i zwykle kończy się rozmową handlową, a nie wpisaniem karty. Znowu bez kwot.
Ścieżka z Cypressa, uczciwie
Zostaje wtyczka z wpisu z 2020, czyli cypress-image-snapshot. Jeżeli ktoś ma dziś zestaw testów na Cypressie i chce porównywać obrazki, to jest droga najkrótsza, bo cy.matchImageSnapshot() wstawia się w istniejący test jedną linijką i cała reszta zostaje bez zmian.
Muszę tu jednak powiedzieć coś, czego w tamtym wpisie być nie mogło. Ostatnie wydanie tej wtyczki, 4.0.1, jest ze stycznia 2021, a Cypress przez ten czas przeszedł dwa duże wydania. Dziesiątka z 1 czerwca zmieniła strukturę konfiguracji i model wtyczek, więc plik cypress/plugins/index.js, do którego w 2020 roku wklejałem addMatchImageSnapshotPlugin, w nowym układzie po prostu nie jest już punktem wejścia. Jedenastka, którą mam dziś w 11.0.1 z 10 listopada, idzie dalej tą samą drogą.
Nie sprawdziłem, czy 4.0.1 wstaje na Cypressie 11 bez forka, i nie napiszę, że to jest instalacja drop-in, bo tego nie wiem. Uczciwa wersja brzmi tak: przepis z 2020 roku dotyczy Cypressa z tamtej epoki i tam działa. Na dziesiątce i wyżej pierwszym krokiem jest sprawdzenie, czy wtyczka nadal jest utrzymywana i czy ktoś przeniósł ją na setupNodeEvents, a nie kopiowanie mojego starego listingu. Jeżeli odpowiedź wypadnie źle, to nie jest powód do przepisywania całej suite. To jest powód, żeby nowe testy wizualne pisać tam, gdzie ta funkcja jest w rdzeniu narzędzia, czyli w Playwrighcie, dokładnie w duchu kwietniowej decyzji: nowe obszary idą do nowego narzędzia, stare zielone testy zostają na miejscu.
lost-pixel, czyli notatka na przyszłość
Wspominam o nim, żeby nie wyglądało, że nie zauważyłem. lost-pixel jest w npm od 4 czerwca tego roku, aktualna wersja to 2.23.0 z 6 października. Robi zrzuty ze Storybooka albo Ladle, porównuje je i ma gotową akcję na GitHuba, więc dla projektu z biblioteką komponentów wygląda to zgrabnie.
I na tym poprzestaję. Paczka ma w npm pięć miesięcy, hostowana platforma jest dopiero zapowiadana, a ja porównuję dziś narzędzia, na których chcę oprzeć coś, co ma przetrwać rok. Zapisuję na liście do obejrzenia w przyszłym roku, nie stawiam jej obok Percy jako alternatywy i nie polecam nikomu wymiany działającego Backstopa na to.
Przy okazji jedna rzecz z tej samej półki, której również nie biorę: testów komponentowych w Playwrighcie. Paczki @playwright/experimental-ct-* są w narzędziu od maja i nadal mają w nazwie słowo, które wszystko wyjaśnia. Nie buduję na tym wizualnej regresji komponentów w listopadzie 2022.
Gdzie to mieszka w pipelinie
Wybór narzędzia jest mniej ważny niż miejsce, w którym je uruchamiam, więc dopisuję to osobno.
Porównywanie obrazków nie jest wymaganym checkiem na pull requeście. Wpada u mnie do trzeciego koszyka z wrześniowego podziału, czyli do nocnego harmonogramu, i to nie dlatego, że jest wolne, tylko dlatego, że jest z natury zależne od środowiska. Test wizualny, który blokuje merdż, będzie po dwóch tygodniach zjawiskiem meteorologicznym, a nie bramką.
Zrzuty i diffy muszą wyjechać z agenta jako artefakty, inaczej czerwony wynik jest bezużyteczny. Mechanika jest ta sama, którą ustawiałem przy Cypressie na Azure DevOps, zmienia się tylko lista ścieżek do spakowania.
Wszystkie przebiegi wizualne trzymam w jednym obrazie kontenera i na jednym silniku. Wzorzec jest przypięty do platformy, a równoległość na kilku maszynach ma sens tylko wtedy, gdy wszystkie plasterki renderują tak samo. Funkcjonalnie sprawdzam trzy przeglądarki, wizualnie na razie jedną, bo trzy razy tyle wzorców to trzy razy tyle akceptacji.
I rzecz, o której łatwo zapomnieć po miesiącu spędzonym na warstwie API. Testy integracyjne w pamięci procesu, które opisywałem w październiku, są szybkie i deterministyczne, i nie powiedzą absolutnie nic o tym, że ktoś podmienił klasę przycisku i formularz zniknął pod stopką. To są odpowiedzi na inne pytania i jedno nie zastępuje drugiego.
Jak wybieram w listopadzie 2022
| Narzędzie | Wersja na dziś | Gdzie wzorzec | Kto zatwierdza | Koszt |
|---|---|---|---|---|
Playwright toHaveScreenshot |
1.27.1 |
plik w repo | autor, w commicie | zero, poza wagą repo |
| BackstopJS | 6.1.4 |
plik w repo | autor, w commicie | zero, plus drugi runner |
Percy + @percy/playwright |
1.0.4 |
usługa | dowolna osoba w UI | za snapshot |
Applitools @applitools/eyes-playwright |
1.12.7 |
usługa | dowolna osoba w UI | licencja |
cypress-image-snapshot |
4.0.1 |
plik w repo | autor, w commicie | zero, utrzymanie pod znakiem zapytania |
| lost-pixel | 2.23.0 |
plik w repo | autor, w commicie | zero, paczka ma pięć miesięcy |
Z tego wychodzą cztery decyzje, które podejmuję na dziś.
Mały lub średni zestaw w TypeScripcie, jedna aplikacja, zespół deweloperski: zostaję przy Playwright core. Zero nowych zależności, wzorzec obok testu, próg w jednym miejscu i ta sama komenda co dla reszty testów.
Siatka wielu adresów na kilku szerokościach, bez logowania i bez scenariuszy: Backstop. Konfiguracja jest deklaratywna, raport czytelny, a to, że jest to osobny runner, przestaje boleć, kiedy i tak nie mam do czego go doklejać.
Wygląd, o którym decyduje ktoś spoza repozytorium, albo strona z dużą ilością zmiennych danych: usługa. Percy, kiedy najważniejszy jest prosty przepływ akceptacji, Eyes, kiedy najważniejsze są tryby dopasowania i renderowanie macierzy po ich stronie. W obu wypadkach zaczynam od policzenia snapshotów i od zgody na wysyłanie zrzutów na zewnątrz.
Działający zestaw na Cypressie: nie ruszam. Wtyczka z 2020 roku zostaje tam, gdzie jest, o ile stoi na wersji Cypressa, dla której powstała. Nowe testy wizualne dopisuję w Playwrighcie i nie robię z tego migracji.
Podsumowanie
Trzy rzeczy, które zabieram z tego porównania.
Najtańsze narzędzie w tej kategorii to nie to, które jest darmowe, tylko to, w którym akceptacja poprawnej zmiany kosztuje najmniej. Wszystko inne rozstrzyga się w drugiej kolejności.
Wzorzec jest artefaktem projektu, nie efektem ubocznym testu. Trzymany w gicie jest związany z platformą i wymaga dyscypliny co do maszyny, która go wyprodukowała. Trzymany w usłudze wymaga budżetu i zgody na wysłanie obrazów aplikacji na zewnątrz. Trzeciej możliwości nie ma i wybór jednej z dwóch to jest właśnie ta decyzja, którą odkładałem od sierpnia.
Wizualna regresja nie odpowiada na pytanie, co warto sprawdzać. Odpowiada wyłącznie na pytanie, czy to wygląda tak jak wczoraj. Rok temu w podsumowaniu roku zapisałem sobie, że najpierw izolacja, potem reszta, i tutaj obowiązuje ta sama kolejność: zestaw, który migocze funkcjonalnie, po dołożeniu obrazków będzie migotał dwa razy. Co w ogóle zasługuje na sprawdzanie przez interfejs, zostaje osobną rozmową, której żadna z tych pięciu paczek za mnie nie odbędzie.
Czego nie wiem po tym tygodniu czytania: czy tryby dopasowania z Eyes realnie wytrzymują aplikację z dużą ilością zmiennych danych i czy próg maxDiffPixelRatio, który dziś wpisałem z palca, przetrwa pierwszy miesiąc w nocnym przebiegu. Obie liczby sprawdzę na własnym zestawie i wtedy o nich napiszę.

