Playwright sam w sobie jest bardzo szybkim narzędziem do automatyzacji testów. Problem zaczyna się dopiero wtedy, gdy projekt rośnie, testów robi się coraz więcej, a cały zestaw testów, który kiedyś wykonywał się w minutę, nagle zaczyna działać 10, 15 albo 30 minut.
I wtedy pojawia się klasyczne pytanie: czy da się to jakoś przyspieszyć?
Da się.
Oczywiście nie chcę tutaj obiecywać magicznie, że każdy projekt przyspieszymy dokładnie o 50%. To zależy od aplikacji, środowiska, liczby testów, jakości testów i konfiguracji pipeline’u. Ale w praktyce bardzo często okazuje się, że testy nie są wolne dlatego, że Playwright jest wolny. One są wolne, bo robimy kilka rzeczy w nieoptymalny sposób.
Najczęstsze problemy to:
- logowanie użytkownika w każdym teście,
- sztuczne
waitForTimeout, - za dużo testów uruchamianych w jednej przeglądarce i jednym wątku,
- brak równoległego uruchamiania,
- testy zależne od siebie,
- przygotowanie danych przez UI zamiast przez API,
- uruchamianie wszystkich testów na wszystkich przeglądarkach za każdym razem,
- zbyt duże testy end-to-end, które sprawdzają zbyt wiele naraz.
W tym wpisie pokażę kilka praktycznych sposobów, które mogą realnie skrócić czas wykonywania testów Playwright.
1. Najpierw zmierz, co naprawdę trwa najdłużej
Zanim zaczniemy cokolwiek optymalizować, warto najpierw sprawdzić, gdzie faktycznie tracimy czas.
To jest ważne, bo czasami wydaje nam się, że problemem jest sam Playwright, a po chwili okazuje się, że największy koszt generuje na przykład:
- logowanie,
- tworzenie danych testowych,
- oczekiwanie na odpowiedzi API,
- przechodzenie przez kilka ekranów aplikacji,
- powtarzające się kroki w każdym teście.
Na początek możesz uruchomić testy normalnie:
npx playwright testA potem wygenerować raport HTML:
npx playwright show-reportW raporcie bardzo szybko zobaczysz, które testy trwają najdłużej. I to właśnie od nich warto zacząć.
Nie ma sensu optymalizować testu, który trwa 2 sekundy, jeśli obok masz test, który trwa 45 sekund i wykonuje logowanie, tworzenie konta, przejście przez kilka formularzy i jeszcze sprawdzenie maila.
Najpierw szukamy największych strat. Dopiero później poprawiamy szczegóły.
2. Włącz równoległe uruchamianie testów
Jedną z największych zalet Playwrighta jest to, że testy mogą być uruchamiane równolegle.
Czyli zamiast wykonywać testy jeden po drugim, Playwright może uruchomić kilka procesów jednocześnie. Dzięki temu cały zestaw testów może zakończyć się dużo szybciej.
Przykładowa konfiguracja w playwright.config.ts może wyglądać tak:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
workers: process.env.CI ? 4 : undefined,
retries: process.env.CI ? 1 : 0,
reporter: 'html',
use: {
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});Najważniejsze są tutaj dwie rzeczy:
fullyParallel: trueoraz:
workers: process.env.CI ? 4 : undefinedPierwsza opcja pozwala uruchamiać testy w pełni równolegle. Druga określa liczbę workerów, czyli procesów, które mogą wykonywać testy w tym samym czasie.
Jeśli masz 100 testów i każdy trwa kilka sekund, równoległe uruchamianie potrafi zrobić ogromną różnicę.
Trzeba jednak uważać na jedną rzecz. Testy równoległe muszą być od siebie niezależne. Jeżeli jeden test tworzy dane, a drugi test zakłada, że te dane już istnieją, to przy równoległym uruchamianiu mogą pojawić się losowe błędy.
Dlatego dobra zasada jest taka:
każdy test powinien sam przygotować sobie dane albo korzystać z izolowanego zestawu danych.
Nie powinno być sytuacji, w której test B działa tylko dlatego, że wcześniej wykonał się test A.
3. Nie loguj użytkownika w każdym teście
To jest jeden z najczęstszych powodów, przez które testy Playwright działają wolno.
Wyobraź sobie, że masz 80 testów. I każdy test robi to samo:
- otwiera stronę logowania,
- wpisuje email,
- wpisuje hasło,
- klika przycisk logowania,
- czeka na dashboard,
- dopiero wtedy zaczyna właściwy test.
Jeśli logowanie trwa 5 sekund, to przy 80 testach tracisz ponad 6 minut tylko na samo logowanie.
A przecież w większości przypadków nie testujesz logowania w każdym teście. Logowanie jest tylko warunkiem wejścia do aplikacji.
Lepsze podejście to zapisanie stanu zalogowanego użytkownika i ponowne użycie go w kolejnych testach.
Można przygotować plik setup, na przykład:
import { test as setup, expect } from '@playwright/test';
setup('login', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByText('Dashboard')).toBeVisible();
await page.context().storageState({ path: 'playwright/.auth/user.json' });
});A potem w konfiguracji możesz użyć tego stanu:
use: {
storageState: 'playwright/.auth/user.json',
}Dzięki temu testy startują już jako zalogowany użytkownik.
To często daje ogromną oszczędność czasu, szczególnie w aplikacjach, gdzie logowanie jest wolne, wymaga dodatkowych przekierowań, MFA, ładowania dashboardu albo pobierania dużej ilości danych po zalogowaniu.
Oczywiście nadal warto mieć osobne testy dla samego logowania. Ale nie ma sensu testować logowania przy okazji każdego scenariusza.
4. Usuń sztuczne timeouty
Jeśli widzisz w projekcie coś takiego:
await page.waitForTimeout(3000);to prawdopodobnie masz dobry kandydat do optymalizacji.
Sztuczne timeouty są wygodne, ale bardzo często niepotrzebnie spowalniają testy. Jeśli w teście masz 10 takich timeoutów po 3 sekundy, to właśnie dodałeś 30 sekund oczekiwania. Nawet jeśli aplikacja była gotowa po pół sekundy.
Zamiast tego lepiej używać lokatorów i asercji, które same czekają na odpowiedni stan elementu.
Zamiast:
await page.click('#save');
await page.waitForTimeout(3000);
await expect(page.locator('.success')).toBeVisible();lepiej napisać:
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Saved successfully')).toBeVisible();Playwright potrafi poczekać na element, jego widoczność i możliwość wykonania akcji. Nie musisz ręcznie zgadywać, czy aplikacja potrzebuje 1, 2 czy 3 sekund.
Sztuczne timeouty zostawiłbym tylko w bardzo wyjątkowych sytuacjach. Na przykład wtedy, gdy naprawdę chcesz sprawdzić zachowanie aplikacji po określonym czasie. Ale jako standardowy sposób „stabilizowania” testów — zdecydowanie nie.
5. Nie uruchamiaj wszystkiego na każdej przeglądarce za każdym razem
Playwright pozwala testować aplikację w Chromium, Firefox i WebKit. To świetna funkcja, ale nie zawsze musimy używać jej przy każdym uruchomieniu testów.
Jeżeli przy każdym commicie uruchamiasz wszystkie testy na trzech przeglądarkach, to naturalnie czas testów może wzrosnąć nawet kilkukrotnie.
Lepsze podejście może wyglądać tak:
- przy każdym pull requeście uruchamiasz szybki zestaw testów w Chromium,
- raz dziennie uruchamiasz pełną regresję na Chromium, Firefox i WebKit,
- przed wydaniem produkcyjnym uruchamiasz pełny zestaw testów na wszystkich przeglądarkach.
Przykładowo w konfiguracji możesz mieć kilka projektów:
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
]Ale w szybkim pipeline możesz uruchomić tylko Chromium:
npx playwright test --project=chromiumTo jest bardzo praktyczne podejście. Nadal masz testy cross-browser, ale nie marnujesz czasu na pełną regresję przy każdej małej zmianie.
6. Przygotowuj dane przez API, a nie przez UI
To kolejny częsty problem.
Załóżmy, że chcesz przetestować edycję zamówienia. Wiele osób robi to tak:
- loguje się,
- przechodzi do formularza tworzenia zamówienia,
- wypełnia formularz,
- zapisuje zamówienie,
- przechodzi do listy zamówień,
- otwiera szczegóły,
- dopiero wtedy testuje edycję.
Taki test może działać, ale jest wolny. Co więcej, jeśli tworzenie zamówienia przez UI się zepsuje, to test edycji też poleci, mimo że sama edycja może działać poprawnie.
Lepsze podejście: dane potrzebne do testu przygotować przez API.
Czyli zamiast klikać przez kilka ekranów, robimy request, tworzymy dane i od razu przechodzimy do właściwego scenariusza.
Przykład:
test('user can edit order', async ({ page, request }) => {
const response = await request.post('/api/orders', {
data: {
productId: 1,
quantity: 2,
customerName: 'Test Customer',
},
});
const order = await response.json();
await page.goto(`/orders/${order.id}`);
await page.getByRole('button', { name: 'Edit' }).click();
await page.getByLabel('Quantity').fill('3');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Order updated')).toBeVisible();
});Dzięki temu test skupia się na tym, co faktycznie ma sprawdzić: edycji zamówienia.
Nie traci czasu na klikanie po aplikacji tylko po to, żeby przygotować dane.
7. Dziel testy na szybkie smoke testy i pełną regresję
Nie każdy test musi być uruchamiany zawsze.
To bardzo ważne, szczególnie w większych projektach. Jeśli masz 300 testów end-to-end, to uruchamianie ich po każdej małej zmianie może być po prostu niepraktyczne.
Dlatego warto podzielić testy na grupy:
- smoke,
- regression,
- critical path,
- visual,
- API,
- e2e.
Przykład oznaczenia testu:
test('user can login @smoke', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByText('Dashboard')).toBeVisible();
});Potem możesz uruchomić tylko testy smoke:
npx playwright test --grep @smokeA pełną regresję odpalić osobno, na przykład na noc albo przed wdrożeniem.
To podejście bardzo dobrze sprawdza się w CI/CD. Programista dostaje szybką informację zwrotną, czy najważniejsze funkcje działają, a pełne testy mogą wykonać się w innym etapie pipeline’u.
8. Uważaj na zbyt duże testy end-to-end
Nie każdy scenariusz musi być ogromnym testem end-to-end.
Czasami widzę testy, które robią wszystko:
- logowanie,
- tworzenie klienta,
- tworzenie zamówienia,
- płatność,
- fakturę,
- wysyłkę,
- anulowanie,
- sprawdzenie maila,
- sprawdzenie historii.
Taki test może mieć sens jako jeden scenariusz biznesowy. Ale jeśli cały projekt składa się z takich testów, to będzie wolno, niestabilnie i trudno w utrzymaniu.
Lepiej mieć mniej długich testów end-to-end, a więcej krótszych testów, które sprawdzają konkretne funkcje.
Przykład:
- osobny test dla logowania,
- osobny test dla utworzenia zamówienia,
- osobny test dla edycji zamówienia,
- osobny test dla anulowania zamówienia,
- osobny test dla uprawnień użytkownika.
Długie scenariusze zostawiłbym tylko dla naprawdę krytycznych ścieżek biznesowych.
Dzięki temu testy są szybsze, prostsze do debugowania i mniej podatne na losowe błędy.
9. Ogranicz nagrywanie video i trace
Playwright pozwala nagrywać video, robić screenshoty i zapisywać trace. To bardzo przydatne przy debugowaniu, szczególnie w CI.
Ale jeśli zapisujesz wszystko zawsze, dla każdego testu, to może to spowalniać cały proces i generować dużo niepotrzebnych plików.
Dlatego zamiast:
use: {
video: 'on',
trace: 'on',
screenshot: 'on',
}często lepiej użyć:
use: {
video: 'retain-on-failure',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
}Wtedy materiały diagnostyczne zapisują się głównie wtedy, gdy są naprawdę potrzebne.
To jest dobry kompromis. Nadal mamy dane do analizy błędów, ale nie obciążamy każdego udanego testu dodatkowymi operacjami.
10. Ustaw rozsądne timeouty
Timeouty są potrzebne, ale zbyt wysokie wartości mogą maskować problemy.
Jeśli każdy test może czekać 60 sekund na element, to błędny test będzie bardzo długo wisiał, zanim zakończy się porażką.
Przykład konfiguracji:
export default defineConfig({
timeout: 30 * 1000,
expect: {
timeout: 5000,
},
use: {
actionTimeout: 10000,
navigationTimeout: 15000,
},
});Nie chodzi o to, żeby ustawić timeouty ekstremalnie nisko. Chodzi o to, żeby były rozsądne.
Jeśli aplikacja normalnie ładuje ekran w 2 sekundy, to czekanie 60 sekund na każdy element raczej nie ma sensu. Lepiej szybciej wykryć problem i poprawić test albo aplikację.
11. Nie przesadzaj z beforeEach
beforeEach jest bardzo wygodne, ale łatwo zrobić z niego śmietnik.
Na przykład:
test.beforeEach(async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Login' }).click();
await page.goto('/dashboard');
await page.getByRole('button', { name: 'Accept cookies' }).click();
});Jeśli taki kod wykonuje się przed każdym testem, to bardzo szybko zaczyna robić się drogo.
Czasami lepiej:
- użyć
storageState, - przygotować dane przez API,
- wejść od razu na konkretny adres,
- nie wykonywać tych samych kroków w każdym teście,
- przenieść część logiki do fixture.
Dobrze napisany beforeEach powinien przygotować tylko to, co jest naprawdę potrzebne dla danego testu.
12. Uruchamiaj testy w shardach
Jeżeli masz bardzo dużo testów, możesz podzielić je na części, czyli shardy.
Przykład:
npx playwright test --shard=1/4
npx playwright test --shard=2/4
npx playwright test --shard=3/4
npx playwright test --shard=4/4Dzięki temu możesz rozdzielić testy na kilka osobnych maszyn albo jobów w CI.
To ma sens szczególnie wtedy, gdy projekt jest już większy i samo zwiększenie liczby workerów na jednej maszynie nie daje wystarczającego efektu.
W praktyce wygląda to tak, że zamiast jednego pipeline’u trwającego 20 minut, możesz mieć kilka równoległych jobów, które kończą się dużo szybciej.
13. Popraw lokatory
Słabe lokatory też mogą spowalniać testy i powodować niestabilność.
Jeśli testy często szukają elementów po skomplikowanych selektorach CSS albo XPath, to może być trudniej je utrzymać. Dodatkowo takie testy częściej psują się po zmianach w HTML.
Zamiast:
await page.locator('div:nth-child(3) > button').click();lepiej użyć:
await page.getByRole('button', { name: 'Save' }).click();Albo:
await page.getByTestId('save-button').click();Dobre lokatory nie tylko poprawiają czytelność testów. One też zmniejszają liczbę sytuacji, w których Playwright musi długo czekać albo trafia w niewłaściwy element.
Najczęściej korzystam z:
getByRole,getByLabel,getByText,getByPlaceholder,getByTestId.
Test powinien być czytelny. Kiedy ktoś otwiera plik testowy, powinien mniej więcej rozumieć, co się dzieje, bez analizowania całej struktury HTML.
14. Nie testuj wszystkiego przez UI
To może zabrzmieć kontrowersyjnie, ale nie wszystko powinno być testem UI.
Testy end-to-end są wartościowe, ale są też najdroższe. Uruchamiają przeglądarkę, klikają po aplikacji, czekają na renderowanie, komunikują się z backendem i często korzystają z wielu zależności.
Dlatego warto zadać sobie pytanie:
Czy ten przypadek naprawdę musi być testowany przez UI?
Czasami lepiej użyć:
- testów jednostkowych,
- testów integracyjnych,
- testów API,
- testów komponentów,
- kilku testów E2E dla krytycznych ścieżek.
Dobra strategia testowania nie polega na tym, żeby wszystko automatyzować przez przeglądarkę. Chodzi o to, żeby sprawdzać funkcje na odpowiednim poziomie.
Jeżeli walidację formularza możesz dobrze sprawdzić na poziomie komponentu, to nie zawsze potrzebujesz do tego pełnego testu end-to-end.
15. Przykładowa szybka konfiguracja Playwright
Poniżej przykład konfiguracji, która może być dobrym punktem startowym:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
timeout: 30 * 1000,
expect: {
timeout: 5000,
},
retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: [['html'], ['list']],
use: {
baseURL: 'https://example.com',
actionTimeout: 10000,
navigationTimeout: 15000,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
},
],
});Nie traktowałbym tego jako idealnej konfiguracji dla każdego projektu. Bardziej jako punkt wyjścia.
W prawdziwym projekcie trzeba dopasować liczbę workerów, timeouty, przeglądarki i sposób przygotowania danych do konkretnej aplikacji oraz środowiska CI.
Co może dać największe przyspieszenie?
Gdybym miał wskazać rzeczy, które najczęściej dają największy efekt, to byłyby to:
- równoległe uruchamianie testów,
- ponowne użycie zalogowanej sesji,
- usunięcie
waitForTimeout, - przygotowanie danych przez API,
- uruchamianie smoke testów osobno od pełnej regresji,
- ograniczenie liczby przeglądarek w szybkim pipeline,
- poprawienie zbyt dużych testów end-to-end.
W wielu projektach już same punkty 1, 2 i 3 potrafią mocno skrócić czas wykonywania testów.
Jeśli wcześniej każdy test logował użytkownika od zera i miał kilka sztucznych timeoutów, to różnica może być naprawdę duża.
Podsumowanie
Przyspieszanie testów Playwright nie polega na jednej magicznej opcji w konfiguracji.
To raczej suma kilku dobrych decyzji:
- piszemy testy niezależne od siebie,
- uruchamiamy je równolegle,
- nie logujemy się w każdym teście,
- unikamy sztucznych timeoutów,
- dane przygotowujemy przez API,
- nie uruchamiamy pełnej regresji po każdej małej zmianie,
- nie testujemy wszystkiego przez UI.
Moim zdaniem to jest jedna z najważniejszych rzeczy w automatyzacji testów. Testy mają nie tylko działać. One mają też dawać szybką informację zwrotną.
Jeżeli testy wykonują się zbyt długo, zespół przestaje traktować je jako pomoc. Zaczyna traktować je jako przeszkodę.
A dobrze napisane testy Playwright powinny działać szybko, stabilnie i jasno pokazywać, czy aplikacja nadal działa tak, jak powinna.
