Sharding, czyli podział zestawu testów na części wykonywane równolegle, to najtańszy sposób na skrócenie regresji Playwright z godziny do kilkunastu minut. U klientów widuję powtarzalny schemat: pełny przebieg E2E trwa 40–60 minut, więc zespół uruchamia go raz dziennie zamiast przy każdym merge’u, a błędy wychodzą w piątek po południu. Poniżej opisuję konfigurację, którą wdrażam w Jenkinsie: shardy generowane dynamicznie, efemeryczne kontenery i scalanie wyników w jeden raport HTML.
Próg bólu ustawiam nisko. Regresja powyżej 30 minut oznacza, że deweloper po pushu przełącza się na inne zadanie i wynik testów czyta już w oderwaniu od kodu, który zmieniał. Powyżej godziny zespoły zaczynają mergować bez czekania na wynik. Dokładanie kolejnych stałych maszyn niewiele tu daje, bo flota i tak czeka na najwolniejszy plik testowy, a rachunek z chmury rośnie liniowo.
Flaga –shard to połowa roboty
Playwright ma sharding wbudowany: npx playwright test --shard=2/6 uruchamia drugą z sześciu części zestawu. Lokalnie albo w prostym skrypcie działa to dobrze i jest jednym z powodów, dla których w porównaniu Selenium czy Playwright punktuję Playwrighta za dojrzałość w CI. W Jenkinsie naiwne użycie tej flagi rodzi trzy problemy:
- Nierówny podział czasu. Playwright dzieli testy plikami, a nie czasem wykonania. Gdy shard 1 dostanie pięć szybkich speców logowania, a shard 2 jeden plik z generowaniem PDF-ów, cała regresja czeka na ten drugi.
- Rozsypane raporty. Sześć shardów to sześć osobnych raportów HTML. Z doświadczenia: nikt ich nie otwiera.
- Sztywna infrastruktura. Sześciu agentów skonfigurowanych na stałe marnuje się, kiedy nocny smoke potrzebuje dwóch maszyn.
Architektura: równoległe stage’e i efemeryczne agenty
Składam rozwiązanie z trzech elementów:
- pętla w Jenkinsfile generuje równoległe zadania, więc liczbę shardów zmieniam parametrem builda, bez edycji pipeline’u;
- agenci to kontenery Docker (u większych klientów pody Kubernetes), które wstają na czas jednego sharda i znikają zaraz po nim, płacimy za minuty procesora, a nie za czekające maszyny;
- reporter blob w Playwright pozwala skleić wyniki ze wszystkich maszyn w jeden raport.
Jak osadzić taki etap w szerszym potoku (build, testy, wdrożenie), opisałem w tekście o testach automatycznych w pipeline CI/CD.
Jenkinsfile: shardy generowane pętlą
pipeline {
agent none
parameters {
string(name: 'SHARD_COUNT', defaultValue: '6', description: 'Na ile czesci podzielic testy')
}
stages {
stage('Regresja rownolegle') {
steps {
script {
def shards = [:]
int total = params.SHARD_COUNT.toInteger()
for (int i = 1; i <= total; i++) {
int nr = i // kopia licznika dla domkniecia
shards["shard-${nr}"] = {
node('playwright-docker') {
checkout scm
sh "npx playwright test --shard=${nr}/${total} --reporter=blob"
stash name: "blob-${nr}", includes: 'blob-report/**'
}
}
}
parallel shards
}
}
}
stage('Scal raporty') {
agent { label 'playwright-docker' }
steps {
script {
int total = params.SHARD_COUNT.toInteger()
for (int i = 1; i <= total; i++) { unstash "blob-${i}" }
}
sh 'npx playwright merge-reports --reporter html ./blob-report'
}
}
}
}Dwa szczegóły, o które zespoły regularnie się potykają. Zmienna nr musi być kopią licznika, bo domknięcie w Groovym przechwyci inaczej ostatnią wartość pętli i wszystkie gałęzie odpalą ten sam shard. Każdy shard stashuje swój katalog blob-report, bo agenci nie współdzielą workspace’u; przy shardingu Playwright sam nazywa archiwa report-1.zip, report-2.zip i tak dalej, więc po unstashu nic się nie nadpisuje.
Jeden raport zamiast sześciu
Reporter --reporter=blob zapisuje wynik sharda jako archiwum zip z kompletem danych: zrzutami ekranu, wideo i trace’ami. Scalanie plików JUnit XML gubi te załączniki, blob je zachowuje. Komenda npx playwright merge-reports --reporter html ./blob-report skleja archiwa w jeden raport, który wygląda tak, jakby cała regresja poszła na jednej maszynie. Manager i tester dostają jeden link, infrastruktura zostaje niewidoczna.
Koszty: gotowy obraz Dockera i instancje spot
Dwie rzeczy trzymają rachunek w ryzach, kiedy zwiększam liczbę shardów do 10–20:
- Gotowy obraz Dockera.
npm ciplus pobieranie przeglądarek w każdym shardzie oznacza ściąganie tych samych pakietów dwadzieścia razy. Obraz znode_modulesi przeglądarkami buduję osobnym pipeline’em przy każdej zmianiepackage-lock.json; start sharda spada z około 3 minut do kilkunastu sekund. - Instancje spot. Testy regresyjne znoszą przerwanie maszyny, więc pasują na instancje spot, w AWS i GCP o 60–90% tańsze od on-demand. Retry wbudowany w Playwright plus
retry(2)na blokunodew Jenkinsie łata przypadki, gdy chmura ubije kontener w trakcie testu.
W projektach, które tak przestawiałem, regresja skraca się o około 70% względem przebiegu sekwencyjnego, a rachunek wychodzi niższy niż za stałą flotę agentów czekających na nocny build.
Balansowanie shardów czasem wykonania
Domyślny sharding jest deterministyczny, ale ślepy na czas trwania testów. Kiedy widzę, że jeden shard kończy po 40 minutach, a reszta po 10, przechodzę na podział oparty na historii: skrypt czyta czasy poszczególnych plików z poprzedniego przebiegu (JUnit XML wystarczy), rozdziela spec-i do koszyków o zbliżonej sumie czasów (klasyczny bin packing) i przekazuje każdemu shardowi jawną listę: npx playwright test tests/login.spec.ts tests/platnosci.spec.ts. To godzina lub dwie pracy inżynierskiej, ale dolny limit czasu regresji wyznacza wtedy najdłuższy pojedynczy test, a nie pechowy przydział plików.
Zaczynam zwykle od 4–6 shardów i patrzę na rozrzut czasów w scalonym raporcie. Dokładanie maszyn powyżej punktu, w którym shardy kończą równo, nie skraca już niczego.