Testowanie API z Playwright
Naucz się używać APIRequestContext Playwright do testowania backendu. Przygotowuj dane testowe przez API przed testami UI, waliduj odpowiedzi, obsługuj nagłówki uwierzytelniania i decyduj, kiedy.
Ten wpis jest częścią serii Podstawy Playwright. Część 4 omówiła auto-waiting i zabijanie flaków. Ten ostatni post bada używanie Playwright do testowania API — potężny wzorzec dla szybszych, bardziej niezawodnych testów.
Dlaczego testowanie API z Playwright?
Większość frameworków do testów E2E skupia się wyłącznie na automatyzacji przeglądarki. Playwright zawiera w pełni funkcjonalny klient HTTP: APIRequestContext. To odblokowuje dwa potężne wzorce:
1. Przygotowywanie danych dla testów UI
Zamiast klikać przez UI, żeby stworzyć dane testowe, wywołaj API bezpośrednio:
test('użytkownik może zobaczyć swoje zamówienie', async ({ page, request }) => {
// Utwórz zamówienie przez API (szybko)
const response = await request.post('/api/orders', {
data: { product: 'Laptop', quantity: 1 }
});
const order = await response.json();
// Testuj UI (to, co widzi użytkownik)
await page.goto(`/orders/${order.id}`);
await expect(page.getByText('Laptop')).toBeVisible();
});
To jest 10x szybsze niż tworzenie zamówienia przez UI. Jest też bardziej niezawodne — nie zależysz od działającego flow checkout tylko po to, żeby skonfigurować swój test.
2. Czyste testowanie API
Możesz pisać testy tylko-API w Playwright bez dotykania przeglądarki:
test('API zwraca poprawny status zamówienia', async ({ request }) => {
const response = await request.get('/api/orders/123');
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const order = await response.json();
expect(order.status).toBe('completed');
});
To działa w milisekundach w porównaniu do sekund dla testów przeglądarki. Używaj tego do testowania logiki backendu, walidacji kontraktów API i wychwytywania regresji wcześnie.
Fixture request
Playwright dostarcza fixture request w każdym teście. To instancja APIRequestContext wstępnie skonfigurowana z:
- Base URL z
playwright.config.ts - Domyślnymi nagłówkami
- Obsługą cookies/sesji
- Automatycznym parsowaniem JSON
Podstawowe żądanie GET
test('pobierz profil użytkownika', async ({ request }) => {
const response = await request.get('/api/users/me');
expect(response.ok()).toBeTruthy();
const user = await response.json();
expect(user.email).toBe('user@example.com');
});
Żądanie POST z body JSON
test('utwórz nowe zamówienie', async ({ request }) => {
const response = await request.post('/api/orders', {
data: {
product: 'Mysz bezprzewodowa',
quantity: 2,
shippingAddress: 'ul. Główna 123'
}
});
expect(response.status()).toBe(201);
const order = await response.json();
expect(order.id).toBeDefined();
expect(order.product).toBe('Mysz bezprzewodowa');
});
Żądanie PUT (aktualizacja)
test('zaktualizuj status zamówienia', async ({ request }) => {
const response = await request.put('/api/orders/123', {
data: { status: 'wysłane' }
});
expect(response.ok()).toBeTruthy();
const updated = await response.json();
expect(updated.status).toBe('wysłane');
});
Żądanie DELETE
test('usuń zamówienie', async ({ request }) => {
const response = await request.delete('/api/orders/123');
expect(response.status()).toBe(204); // No Content
});
Obsługa uwierzytelniania
Większość API wymaga uwierzytelniania. Playwright dostarcza kilka sposobów obsługi tego.
Opcja 1: Przekaż nagłówki bezpośrednio
test('żądanie uwierzytelnione', async ({ request }) => {
const response = await request.get('/api/orders', {
headers: {
'Authorization': 'Bearer abc123token'
}
});
expect(response.ok()).toBeTruthy();
});
Opcja 2: Ustaw domyślne nagłówki w configu
// playwright.config.ts
export default defineConfig({
use: {
extraHTTPHeaders: {
'Authorization': 'Bearer abc123token',
},
},
});
Teraz każde żądanie zawiera nagłówek auth automatycznie.
Opcja 3: Użyj fixture’a dla setupu auth
// fixtures/api.ts
import { test as base } from '@playwright/test';
type AuthFixtures = {
authenticatedRequest: APIRequestContext;
};
export const test = base.extend<AuthFixtures>({
authenticatedRequest: async ({ playwright }, use) => {
const context = await playwright.request.newContext({
baseURL: 'http://localhost:3000',
extraHTTPHeaders: {
'Authorization': 'Bearer abc123token',
},
});
await use(context);
await context.dispose();
},
});
Opcja 4: Zaloguj przez API przed testami
test.beforeAll(async ({ request }) => {
const response = await request.post('/api/auth/login', {
data: { email: 'user@example.com', password: 'password123' }
});
const { token } = await response.json();
process.env.AUTH_TOKEN = token;
});
test('użyj tokena auth', async ({ request }) => {
const response = await request.get('/api/orders', {
headers: {
'Authorization': `Bearer ${process.env.AUTH_TOKEN}`
}
});
expect(response.ok()).toBeTruthy();
});
:::tip[Najlepsza praktyka: Fixture’y dla Auth] Używaj fixture’ów (Opcja 3) dla reużywalnych wzorców auth. To trzyma logikę auth poza indywidualnymi testami i czyni testy bardziej czytelnymi. :::
Przygotowywanie danych testowych przez API
Zabójczy wzorzec: twórz dane przez API, testuj UI.
Przykład: Testuj stronę szczegółów zamówienia
test('użytkownik może zobaczyć szczegóły zamówienia', async ({ page, request }) => {
// Setup: Utwórz zamówienie przez API (szybko)
const createResponse = await request.post('/api/orders', {
data: {
product: 'Klawiatura mechaniczna',
quantity: 1,
price: 89.99
}
});
const order = await createResponse.json();
// Test: Przejdź do strony zamówienia (test UI)
await page.goto(`/orders/${order.id}`);
// Assert: Sprawdź, czy UI wyświetla poprawne dane
await expect(page.getByRole('heading')).toContainText(`Zamówienie #${order.id}`);
await expect(page.getByText('Klawiatura mechaniczna')).toBeVisible();
await expect(page.getByText('89,99 zł')).toBeVisible();
// Teardown: Usuń zamówienie przez API (czysto)
await request.delete(`/api/orders/${order.id}`);
});
Dlaczego to lepsze niż tworzenie zamówienia przez UI:
- Szybsze — wywołanie API zajmuje 100ms, flow UI zajmuje 5 sekund
- Bardziej niezawodne — nie zależy od działającego flow checkout
- Skoncentrowane — testuje stronę szczegółów zamówienia, nie cały proces checkout
Walidacja odpowiedzi API
Playwright dostarcza helpery dla popularnych asercji:
Asercje kodu statusu
expect(response.ok()).toBeTruthy(); // Status 200-299
expect(response.status()).toBe(201);
expect(response.status()).toBeGreaterThanOrEqual(200);
expect(response.status()).toBeLessThan(300);
Walidacja schematu JSON
test('odpowiedź ma poprawną strukturę', async ({ request }) => {
const response = await request.get('/api/orders/123');
const order = await response.json();
// Asercje type-safe
expect(order).toHaveProperty('id');
expect(order).toHaveProperty('status');
expect(order).toHaveProperty('createdAt');
expect(typeof order.id).toBe('string');
expect(typeof order.status).toBe('string');
expect(order.items).toBeInstanceOf(Array);
});
Dla bardziej rygorystycznej walidacji schematu, zintegruj walidator schematu JSON:
import Ajv from 'ajv';
const ajv = new Ajv();
const orderSchema = {
type: 'object',
properties: {
id: { type: 'string' },
status: { type: 'string', enum: ['oczekujące', 'zakończone', 'anulowane'] },
items: { type: 'array' }
},
required: ['id', 'status', 'items']
};
test('odpowiedź pasuje do schematu', async ({ request }) => {
const response = await request.get('/api/orders/123');
const order = await response.json();
const validate = ajv.compile(orderSchema);
expect(validate(order)).toBe(true);
});
Asercje nagłówków
test('odpowiedź ma poprawne nagłówki', async ({ request }) => {
const response = await request.get('/api/orders');
expect(response.headers()['content-type']).toContain('application/json');
expect(response.headers()['cache-control']).toBe('no-cache');
});
Testowanie scenariuszy błędów
Testy API świetnie radzą sobie z testowaniem stanów błędów:
Test 404 Not Found
test('zwraca 404 dla nieistniejącego zamówienia', async ({ request }) => {
const response = await request.get('/api/orders/99999');
expect(response.status()).toBe(404);
const error = await response.json();
expect(error.message).toContain('Zamówienie nie znalezione');
});
Test 400 Bad Request
test('zwraca 400 dla nieprawidłowych danych', async ({ request }) => {
const response = await request.post('/api/orders', {
data: {
product: '', // Nieprawidłowe: pusty produkt
quantity: -1 // Nieprawidłowe: ujemna ilość
}
});
expect(response.status()).toBe(400);
const error = await response.json();
expect(error.errors).toContain('Nazwa produktu jest wymagana');
expect(error.errors).toContain('Ilość musi być dodatnia');
});
Test 401 Unauthorized
test('zwraca 401 bez tokena auth', async ({ request }) => {
const response = await request.get('/api/orders');
expect(response.status()).toBe(401);
});
Test 500 Server Error
test('obsługuje błędy serwera z wdziękiem', async ({ request }) => {
// Wywołaj błąd serwera (np. wysyłając zniekształcone dane)
const response = await request.post('/api/orders', {
data: { invalid: 'payload' }
});
if (response.status() === 500) {
const error = await response.json();
expect(error.message).toBeDefined();
}
});
Kiedy używać testów API vs testów E2E
| Scenariusz | Test API | Test E2E |
|---|---|---|
| Logika backendu | ✅ Szybki, izolowany | ❌ Wolny, kruchy |
| Walidacja danych | ✅ Bezpośredni, precyzyjny | ⚠️ Pośredni (sprawdź UI) |
| Obsługa błędów | ✅ Łatwo wywołać edge case’y | ❌ Trudno symulować |
| Flow auth | ✅ Testuj generowanie/walidację tokenów | ⚠️ Testuj login UI + użycie tokena |
| Podróże użytkownika | ❌ Nie można testować UI | ✅ Pełne doświadczenie użytkownika |
| Regresja wizualna | ❌ Brak przeglądarki | ✅ Screenshoty, diff’y wizualne |
| Dostępność | ❌ Brak DOM | ✅ Role ARIA, czytnik ekranu |
Ogólna zasada: Testuj logikę na najniższym niezawodnym poziomie. Jeśli logikę backendu można przetestować przez API, zrób to. Zarezerwuj testy E2E dla podróży użytkownika widocznych dla użytkownika, które wymagają przeglądarki.
Łączenie testów API i UI
Najpotężniejszy wzorzec: użyj obu.
Wzorzec: Setup API, test UI, teardown API
test('użytkownik może dodać przedmiot do koszyka', async ({ page, request }) => {
// Setup: Utwórz użytkownika i produkt przez API
const userResponse = await request.post('/api/users', {
data: { email: 'test@example.com', password: 'Test1234!' }
});
const user = await userResponse.json();
const productResponse = await request.post('/api/products', {
data: { name: 'Laptop', price: 999 }
});
const product = await productResponse.json();
// Test: Użyj UI, żeby dodać produkt do koszyka
await page.goto('/login');
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Hasło').fill('Test1234!');
await page.getByRole('button', { name: 'Zaloguj' }).click();
await page.goto(`/products/${product.id}`);
await page.getByRole('button', { name: 'Dodaj do koszyka' }).click();
await expect(page.getByText('Przedmiot dodany do koszyka')).toBeVisible();
// Weryfikuj: Sprawdź koszyk przez API
const cartResponse = await request.get('/api/cart', {
headers: { 'Authorization': `Bearer ${user.token}` }
});
const cart = await cartResponse.json();
expect(cart.items).toHaveLength(1);
expect(cart.items[0].productId).toBe(product.id);
// Teardown: Sprzątanie przez API
await request.delete(`/api/users/${user.id}`);
await request.delete(`/api/products/${product.id}`);
});
Ten wzorzec:
- Setup — Szybki, niezawodny (API)
- Test — Skupia się na interakcji użytkownika (UI)
- Weryfikacja — Sprawdza stan backendu (API)
- Teardown — Szybkie sprzątanie (API)
Playwright vs Postman/Newman
| Funkcja | Playwright | Postman/Newman |
|---|---|---|
| Integracja przeglądarki | ✅ Bezproblemowa (to samo narzędzie) | ❌ Osobne narzędzie |
| TypeScript/JavaScript | ✅ Natywne | ⚠️ Ograniczone skryptowanie |
| Integracja CI/CD | ✅ Ten sam pipeline co E2E | ⚠️ Osobny pipeline |
| Testy tylko-API | ✅ Pełne wsparcie | ✅ Zaprojektowane do tego |
| Zarządzanie kolekcjami | ⚠️ Oparte na kodzie (brak GUI) | ✅ Edytor kolekcji GUI |
| Krzywa uczenia | ⚠️ Wymaga JS/TS | ✅ Niska (oparte na GUI) |
Używaj Playwright do testów API jeśli: Już używasz Playwright do testów E2E. Trzymanie testów API i UI w tym samym frameworku upraszcza CI/CD i redukuje rozprzestrzenianie narzędzi.
Używaj Postmana jeśli: Potrzebujesz GUI do ręcznej eksploracji API, lub Twój zespół woli wizualne zarządzanie kolekcjami.
Przykład z rzeczywistości: checkout e-commerce
Przetestujmy pełny flow checkout używając setupu API + testu UI:
test('kompletny flow checkout', async ({ page, request }) => {
// 1. Setup: Utwórz użytkownika, produkt, dodaj do koszyka (wszystko przez API)
const user = await request.post('/api/users', {
data: { email: 'kupujacy@example.com', password: 'Bezpieczne123!' }
}).then(r => r.json());
const product = await request.post('/api/products', {
data: { name: 'Słuchawki', price: 79.99 }
}).then(r => r.json());
await request.post('/api/cart/add', {
headers: { 'Authorization': `Bearer ${user.token}` },
data: { productId: product.id, quantity: 1 }
});
// 2. Test: Flow checkout UI
await page.goto('/login');
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Hasło').fill('Bezpieczne123!');
await page.getByRole('button', { name: 'Zaloguj' }).click();
await page.getByRole('link', { name: 'Koszyk' }).click();
await expect(page.getByText('Słuchawki')).toBeVisible();
await page.getByRole('button', { name: 'Kasa' }).click();
await page.getByLabel('Numer karty').fill('4111111111111111');
await page.getByLabel('Data ważności').fill('12/25');
await page.getByLabel('CVC').fill('123');
await page.getByRole('button', { name: 'Zapłać teraz' }).click();
await expect(page.getByText('Zamówienie potwierdzone')).toBeVisible();
// 3. Weryfikuj: Sprawdź utworzone zamówienie przez API
const orders = await request.get('/api/orders', {
headers: { 'Authorization': `Bearer ${user.token}` }
}).then(r => r.json());
expect(orders).toHaveLength(1);
expect(orders[0].status).toBe('zakończone');
expect(orders[0].total).toBe(79.99);
// 4. Teardown
await request.delete(`/api/users/${user.id}`);
await request.delete(`/api/products/${product.id}`);
});
Ten pojedynczy test waliduje:
- Tworzenie zamówienia w backendzie (API)
- Flow checkout UI (E2E)
- Przetwarzanie płatności (UI + API)
- Integralność danych (weryfikacja API)
Podsumowanie
Możliwości testowania API w Playwright czynią go kompletnym frameworkiem testowym. Nie potrzebujesz już osobnych narzędzi do testów API i UI.
Kluczowe wnioski:
- Używaj wywołań API do przygotowywania danych testowych — 10x szybsze niż setup UI
- Pisz czyste testy API dla logiki backendu — milisekundy zamiast sekund
- Łącz API i UI w jednym teście — setup przez API, test przez UI, weryfikacja przez API
- Testuj stany błędów łatwo — testy API świetnie radzą sobie z edge case’ami
Fixture request to jedna z najbardziej niedocenianych funkcji Playwright. Opanuj ją, a Twoje testy staną się szybsze, bardziej niezawodne i łatwiejsze w utrzymaniu.
Zadanie na ten tydzień: Znajdź jeden test E2E, który spędza większość czasu na przygotowywaniu danych testowych przez UI (tworzenie użytkowników, produktów, zamówień). Refaktoryzuj go, żeby używał setupu API. Zmierz czas wykonania przed/po. Prawdopodobnie zobaczysz 5-10x przyspieszenie.
To kończy serię Podstawy Playwright. Masz teraz fundament do budowy szybkich, niezawodnych, utrzymywalnych suit testów E2E. Reszta to praktyka.
Poprzednio: Część 4 — Auto-waiting, asercje i walka z flakami