Why Playwright for API testing?Pourquoi Playwright pour les tests API ?

Most people know Playwright as a browser automation tool. But it also ships a powerful API context — a lightweight HTTP client built right into the framework. The advantage? You write your API tests in the same codebase, with the same fixtures, the same reporters, and the same CI pipeline as your end-to-end tests. La plupart des gens connaissent Playwright comme outil d'automatisation de navigateur. Mais il embarque aussi un puissant contexte API — un client HTTP léger intégré directement au framework. L'avantage ? Tu écris tes tests API dans la même base de code, avec les mêmes fixtures, les mêmes reporters et le même pipeline CI que tes tests end-to-end.

No Postman collections to maintain separately. No context switching. Just TypeScript, typed assertions, and a clean test runner. Pas de collections Postman à maintenir séparément. Pas de changement de contexte. Juste TypeScript, des assertions typées et un test runner propre.

Project setupMise en place du projet

The project targets a RESTful API with protected routes. Authentication relies on a Bearer token retrieved once per test session and injected into every request via a shared fixture. Le projet cible une API RESTful avec des routes protégées. L'authentification repose sur un Bearer token récupéré une fois par session de test et injecté dans chaque requête via une fixture partagée.

fixtures/apiContext.ts
import { test as base, APIRequestContext } from '@playwright/test';

type Fixtures = { apiContext: APIRequestContext };

export const test = base.extend<Fixtures>({
  apiContext: async ({ playwright }, use) => {
    // Create a reusable API context with base URL + auth header
    const context = await playwright.request.newContext({
      baseURL: process.env.API_URL,
      extraHTTPHeaders: {
        'Authorization': `Bearer ${process.env.API_TOKEN}`,
        'Content-Type': 'application/json',
      },
    });
    await use(context);
    await context.dispose();
  },
});

Storing the token in an environment variable keeps credentials out of the codebase and makes CI injection straightforward via GitHub Actions secrets. Stocker le token dans une variable d'environnement garde les credentials hors du code et facilite l'injection en CI via les secrets GitHub Actions.

CRUD validationValidation CRUD

Each operation is tested independently with explicit status code assertions and JSON schema validation. Here's the full flow covered: Chaque opération est testée indépendamment avec des assertions explicites sur le code de statut et une validation du schéma JSON. Voici le flux complet couvert :

POST CreateCréer
GET ReadLire
PUT UpdateMettre à jour
DELETE DeleteSupprimer
GET Verify 404Vérifier 404
tests/items.spec.ts
import { test } from '../fixtures/apiContext';
import { expect } from '@playwright/test';

test.describe('Items API', () => {
  let itemId: string;

  test('POST /items – creates a new item', async ({ apiContext }) => {
    const res = await apiContext.post('/items', {
      data: { name: 'Test Item', price: 42 },
    });
    expect(res.status()).toBe(201);            // Created
    const body = await res.json();
    expect(body).toHaveProperty('id');
    expect(body.name).toBe('Test Item');
    itemId = body.id;                           // store for next tests
  });

  test('GET /items/:id – returns correct item', async ({ apiContext }) => {
    const res = await apiContext.get(`/items/${itemId}`);
    expect(res.status()).toBe(200);
    const body = await res.json();
    expect(body.id).toBe(itemId);
  });

  test('POST /items – rejects missing fields', async ({ apiContext }) => {
    const res = await apiContext.post('/items', { data: {} });
    expect(res.status()).toBe(400);            // Bad Request
  });
});

Test isolationIsolation des tests

Each test suite is responsible for cleaning up the data it creates. A dedicated teardown step deletes every resource created during the run, so tests never pollute each other — regardless of execution order. Chaque suite de tests est responsable de nettoyer les données qu'elle crée. Une étape de teardown dédiée supprime chaque ressource créée durant le run, les tests ne se polluent donc jamais — quel que soit l'ordre d'exécution.

tests/items.spec.ts – teardown
test.afterEach(async ({ apiContext }) => {
  if (itemId) {
    await apiContext.delete(`/items/${itemId}`);
    itemId = undefined;
  }
});
  • 🔒
    No shared statePas d'état partagé
    Each test creates its own resources and cleans them up — no dependency between tests. Chaque test crée ses propres ressources et les nettoie — aucune dépendance entre tests.
  • Parallel-safeSûr en parallèle
    Because tests are isolated, Playwright can run them in parallel without race conditions. Comme les tests sont isolés, Playwright peut les exécuter en parallèle sans race condition.
  • 🧹
    Clean environmentEnvironnement propre
    Even on test failure, the afterEach hook runs — the database stays clean. Même en cas d'échec, le hook afterEach s'exécute — la base reste propre.

Zephyr Scale reportingRapport Zephyr Scale

Results are automatically pushed to Zephyr Scale after each run via a custom reporter. Each test is mapped to a Zephyr test case key — the reporter reads the result and updates the execution status (Pass / Fail / Blocked) directly in Jira. Les résultats sont automatiquement poussés vers Zephyr Scale après chaque run via un reporter personnalisé. Chaque test est mappé à une clé de cas de test Zephyr — le reporter lit le résultat et met à jour le statut d'exécution (Pass / Fail / Blocked) directement dans Jira.

playwright.config.ts – reporter
import { defineConfig } from '@playwright/test';

export default defineConfig({
  reporter: [
    ['list'],
    ['./reporters/zephyrReporter.ts', {
      projectKey: process.env.ZEPHYR_PROJECT_KEY,
      token:      process.env.ZEPHYR_TOKEN,
      cycleKey:   process.env.ZEPHYR_CYCLE_KEY,
    }],
  ],
});

💡 Tip : annotate each test with its Zephyr key using a custom tag — test.info().annotations.push({ type: 'zephyr', value: 'PROJ-42' }) — so the reporter can match results without any manual mapping. 💡 Conseil : annoter chaque test avec sa clé Zephyr via un tag personnalisé — test.info().annotations.push({ type: 'zephyr', value: 'PROJ-42' }) — pour que le reporter puisse matcher les résultats sans mapping manuel.

Running in CI with GitHub ActionsExécution en CI avec GitHub Actions

The test suite runs on every push to main. Credentials are stored as GitHub secrets and injected at runtime — no token ever touches the codebase. La suite de tests s'exécute à chaque push sur main. Les credentials sont stockés en secrets GitHub et injectés au runtime — aucun token ne touche jamais le code.

.github/workflows/api-tests.yml
name: API Tests
on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: npx playwright test --project=api
        env:
          API_URL:           ${{ secrets.API_URL }}
          API_TOKEN:         ${{ secrets.API_TOKEN }}
          ZEPHYR_TOKEN:      ${{ secrets.ZEPHYR_TOKEN }}
          ZEPHYR_PROJECT_KEY: ${{ secrets.ZEPHYR_PROJECT_KEY }}
          ZEPHYR_CYCLE_KEY:  ${{ secrets.ZEPHYR_CYCLE_KEY }}

Key takeawaysCe qu'il faut retenir

  • 🎭
    One framework, two layersUn seul framework, deux couches
    Playwright handles both UI and API tests — shared fixtures, shared reporters, shared CI. Playwright gère les tests UI et API — fixtures, reporters et CI partagés.
  • Status codes are not enoughLes codes de statut ne suffisent pas
    Always validate the response body structure — a 200 with a broken payload is still a bug. Toujours valider la structure du corps de réponse — un 200 avec un payload cassé reste un bug.
  • 🗑️
    Clean up what you createNettoie ce que tu crées
    Teardown is not optional. A polluted test environment leads to flaky tests and false positives. Le teardown n'est pas optionnel. Un environnement pollué entraîne des tests instables et de faux positifs.
  • 📊
    Automate the reportingAutomatise le reporting
    Pushing results to Zephyr automatically removes a manual step and keeps traceability intact. Pousser les résultats vers Zephyr automatiquement supprime une étape manuelle et maintient la traçabilité.