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.
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:
expect(page.getByText('Hotovo')).toBeVisible()expect.pollna 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.
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
waitForTimeoutpomocíexpect.pollaexpect.toPasspro dynamické čekání. - Idempotence přes
X-Idempotency-Keyeliminuje 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
Zatím žádné komentáře.