Zpět na domů

Stabilizace testů Playwright bez flaků

Článek rozebírá příčiny flaků v Playwright E2E-testech a navrhuje řešení: dynamické čekání expect.poll, idempotence, víceúrovňové mocky až po Pact, strategie čištění testovacích dat. Praktické příklady kódu pro stabilitu v CI.

Boj s flaki v Playwright: poll, mocky, kontrakty
Advertisement 728x90

Stabilizace E2E testů: Boj proti flaky testům bez waitForTimeout

V CI/CD prostředích E2E testy v Playwright často flakyují kvůli zvýšené zátěži, zpoždění sítě a konkurenci o zdroje. Lokálně výkonný CPU a nízká latence maskují race conditions: frontend vykreslil UI, ale backend ještě nepotvrdil transakci v databázi. V CI jsou podmínky horší — testy vidí nekonzistentní stav a padají náhodně.

Klíč ke stabilitě: přechod od statických zpoždění k dynamickému čekání, idempotence požadavků a spolehlivá izolace závislostí. Probereme techniky, které snižují flaky chování při velkých objemech testovacích běhů.

Dynamické čekání místo pevných timeoutů

waitForTimeout(5000) — antipattern. V 90 % případů flaky vzniká z nesouladu timingů: backend odpověděl za 5001 ms místo očekávaných 5000. Řešení — expect.poll, který dotazuje stav až do úspěchu nebo timeoutu.

Google AdInline article slot
await expect.poll(async () => {
  const order = await api.getOrder(id);
  return order.status;
}, {
  message: 'Očekáváme stav PAID',
  timeout: 30000,
}).toBe('PAID');

Playwright automaticky volí intervaly dotazů. Pro pomalé scénáře nastavte test.setTimeout(60000), ale intervaly neměňte manuálně.

Past networkidle a alternativy

waitUntil: 'networkidle' čeká 500 ms bez síťové aktivity. Problém: analytika, chatovací widgety nebo pingy narušují klid, test visí až do globálního timeoutu.

Pravidlo: čekejte na konkrétní elementy nebo API odpovědi:

Google AdInline article slot
  • expect(page.getByText('Hotovo')).toBeVisible()
  • expect.poll na stav backendu

Pro opakování bloků akcí použijte expect.toPass:

await expect(async () => {
  await page.getByRole('button', { name: 'Aktualizovat' }).click();
  await expect(page.getByText('Stav: Hotovo')).toBeVisible();
}).toPass({
  timeout: 15000
});

Idempotence pro ochranu před síťovými poruchami

V distribuovaných systémech retry při síťovém problému vytváří duplikáty: první POST proběhl, odpověď se ztratila, opakovaný požadavek duplikuje entitu. Výsledek — chyby 400 nebo nekonzistentní stav.

Řešení: generujte Idempotency-Key na základě metody, URL a dat.

Google AdInline article slot
import { createHash } from 'crypto';

export function generateIdempotencyKey(method: string, url: string, data: any): string {
  const payload = `${method}:${url}:${JSON.stringify(data)}`;
  return createHash('sha256').update(payload).digest('hex').slice(0, 16);
}

// V BaseApiClient
protected async post(url: string, data?: any) {
  const key = generateIdempotencyKey('POST', url, data);
  return await this.request.post(url, {
    data,
    headers: { 'X-Idempotency-Key': key }
  });
}

Backend podle klíče vrátí cachovaný výsledek. Flaky chování z retry zmizí.

Mocky: úrovně izolace od jednoduchých po kontrakty

Native mocks s page.route

Pro izolaci UI od backendu:

await page.route('**/api/orders', route => {
  route.fulfill({
    status: 500,
    body: JSON.stringify({ error: 'Internal Server Error' })
  });
});

await page.goto('/orders');
await expect(page.getByText('Něco se pokazilo')).toBeVisible();

Omezení: nezachytává server-side požadavky přes request-fixture.

Infrastrukturní mocky (WireMock)

Backend závisí na externích službách (platby, SMS)? Spusťte WireMock:

# docker-compose.yml
services:
  wiremock:
    image: wiremock/wiremock:3.3.1
    ports:
      - "8080:8080"
    volumes:
      - ./wiremock/mappings:/home/wiremock/mappings

Příklad mapování (payment.json):

{
  "request": {
    "method": "POST",
    "url": "/v1/payments"
  },
  "response": {
    "status": 200,
    "jsonBody": {
      "payment_id": "pay_test_123",
      "status": "succeeded"
    }
  }
}

Testy nezávisí na externím uptime.

Contract Testing proti lživým mockům

Mocky lžou: frontend očekává order_id, backend vrátil orderId — testy projdou, produkce spadne.

Consumer-Driven Contracts (Pact) ukládají očekávání v JSON kontraktu:

import { PactV3, MatchersV3 } from '@pact-foundation/pact';

const provider = new PactV3({
  consumer: 'frontend-tests',
  provider: 'order-service',
  dir: './pacts',
});

it('vrátí objednávku podle id', async () => {
  await provider
    .given('objednávka ord_123 existuje')
    .uponReceiving('GET /orders/ord_123')
    .withRequest({ method: 'GET', path: '/orders/ord_123' })
    .willRespondWith({
      status: 200,
      body: {
        order_id: MatchersV3.string('ord_123'),
        status: MatchersV3.string('PAID'),
      },
    })
    .executeTest(async (mockServer) => {
      const order = await fetchOrder(mockServer.url, 'ord_123');
      expect(order.status).toBe('PAID');
    });
});

Backend ověřuje kontrakt ve svém CI. Změny API se zachytí před deployem.

Hygiena testovacích dat

Testy znečišťují databázi — výkon klesá, testy flakyují kvůli unikátním omezením.

1. TTL s pozadovým čištěním

await api.createUser({
  email: `test_${Date.now()}@example.com`, 
  expires_at: new Date(Date.now() + 24*60*60*1000).toISOString()
});

PostgreSQL pg_cron:

SELECT cron.schedule('cleanup-test-data', '0 * * * *', $$
  DELETE FROM users WHERE expires_at < NOW() AND is_test = true;
$$);

2. Cleanup Queue

Sbírejte ID do fronty při vytváření, čistěte v global teardown:

protected async post(url: string, data?: any) {
  const response = await this.request.post(url, { data });
  const body = await response.json();
  
  if (body.id) {
    cleanupQueue.push({ url, id: body.id });
  }
  return response;
}

3. Partionování tabulek

CREATE TABLE orders_test (
  id UUID, created_at TIMESTAMP
) PARTITION BY RANGE (created_at);

CREATE TABLE orders_test_2024_06 
  PARTITION OF orders_test
  FOR VALUES FROM ('2024-06-01') TO ('2024-07-01');

Odstranění partice — operace O(1).

Co je důležité:

  • Nahraďte waitForTimeout pomocí expect.poll a expect.toPass pro dynamické čekání.
  • Idempotence přes X-Idempotency-Key eliminuje duplikáty z retry.
  • Evoluce mocků: page.route → WireMock → Pact pro plnou izolaci.
  • TTL + cron/fronty/partice pro hygienu dat.
  • Stabilita — systémový problém: řešte backend a infra spolu s testy.

— Editorial Team

Advertisement 728x90

Číst dál