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

ScenariuszTest APITest 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

FunkcjaPlaywrightPostman/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