Salta al contenuto principale

Mock e network interception avanzati

Articolo della serie Playwright Component Testing Avanzato. Pubblicazione prevista 25 November 2027.

Cosa ne pensi di questo articolo?

Mock e network interception avanzati
Playwright ComponentsFoto:SiddePexels

In Breve

Articolo della serie Playwright Component Testing Avanzato. Pubblicazione prevista 25 November 2027.

Mock e network interception avanzati

Nel testing dei componenti Playwright, la capacità di controllare le risorse di rete è critica. Quando un componente dipende da API esterne, server di file, o servizi terzi, mockarli direttamente nel test riduce la flakiness, migliora la velocità di esecuzione, e consente di testare scenari di errore che altrimenti sarebbero difficili da riprodurre. Questo quarto articolo della serie approfondisce le strategie avanzate di network interception e HTTP mocking in Playwright, partendo dagli strumenti built-in fino a pattern sofisticati come la riscrittura dinamica delle risposte e il rate limiting simulato.

Cosa Imparerai

  • Differenza tra route interception e response mocking in Playwright
  • API page.route() per intercettare richieste HTTP e modificare risposte
  • Context-level interception vs page-level: pro e contro
  • Mocking di risposte JSON, binarie e multipart form data
  • Simulazione di errori HTTP (5xx, timeouts, malformed responses)
  • Intercettazione dinamica: basata su URL, header, body
  • Rate limiting e throttling in test
  • Replay di risposte pre-registrate (HAR - HTTP Archive)
  • Pattern avanzati: chain di handler, priority e fallback
  • Best practice per mocking leggibile e manutenibile

1. Network Interception in Playwright: fondamenti

Playwright offre due livelli di intercettazione di rete: route (a livello HTTP) e network event (passivo, solo tracciamento). La rotta è il meccanismo più potente e flessibile.

Una rotta è una regola che intercetta le richieste in uscita dal browser e permette di: rispondere direttamente dal test senza toccare il server reale, modificare la richiesta prima che raggiunga il server, registrare la richiesta per assertion successive, rallentare deliberatamente la risposta, simulare errori di rete.

La rotta opera in due fasi: request phase (prima che il browser invii), e response phase (prima che il browser riceva). A livello di request, puoi ispezionare il metodo, gli header, il body e decidere di fermare o modificare. A livello di response, puoi intercettare la risposta reale dal server e modificarla, o fornire una risposta fittizia senza contattare il server.

1.1 page.route() - il pattern fondamentale

// Snippet 1 - Intercezione base di una rotta API
import { test, expect } from '@playwright/test';

test('intercept API e mock risposta', async ({ page }) => {
  // Registro una rotta che intercetta TUTTE le richieste verso /api/users
  await page.route('**/api/users', async route => {
    // Posso ispezionare la richiesta
    console.log('Richiesta intercettata:', route.request().url());
    console.log('Metodo:', route.request().method());
    console.log('Header:', route.request().headers());

    // Fornisco una risposta mock senza contattare il server reale
    await route.abort('internetdisconnected'); // simula errore
    // oppure:
    // await route.fulfill({
    //   status: 200,
    //   body: JSON.stringify({ id: 1, name: 'John Doe' })
    // });
  });

  // Adesso navigiamo - la rotta intercetterà automaticamente
  await page.goto('https://example.com');
  // ... assertions ...
});

2. Pattern di mocking per scenari comuni

2.1 Risposta JSON con header Content-Type

// Snippet 2 - Mock di risposte JSON ben formate
await page.route('**/api/products', async route => {
  const products = [
    { id: 1, name: 'Product A', price: 29.99 },
    { id: 2, name: 'Product B', price: 49.99 }
  ];

  await route.fulfill({
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify(products),
    headers: {
      'Cache-Control': 'no-cache',
      'X-Custom-Header': 'test'
    }
  });
});

2.2 Simulazione di errori HTTP

// Snippet 3 - Errori HTTP: 5xx, timeouts, network errors
test('gestione errore 500 dal server', async ({ page }) => {
  await page.route('**/api/payment', async route => {
    await route.fulfill({
      status: 500,
      contentType: 'application/json',
      body: JSON.stringify({
        error: 'Internal Server Error',
        message: 'Database connection failed'
      })
    });
  });

  await page.goto('https://example.com/checkout');
  // Componente deve mostrare messaggio di errore graceful
  await expect(page.locator('[data-testid="error-message"]')).toContainText('Database connection failed');
});

test('simulazione timeout di rete', async ({ page }) => {
  await page.route('**/api/slow-endpoint', async route => {
    // Aspetta 30 secondi, poi interrompi con timeout
    await page.waitForTimeout(30000);
    await route.abort('timedout');
  });

  // Oppure usa route.continue() con delay:
  // await route.continue({ timings: { requestStart: 15000 } });
});

3. Intercettazione dinamica basata su criteri

Spesso non vuoi intercettare tutte le richieste a un endpoint, ma solo quelle che soddisfano criteri specifici. Playwrigth permette di ispezionare request method, URL query string, body, e header per decidere il comportamento dinamicamente.

3.1 Routing in base al metodo HTTP

// Snippet 4 - Diversi mock per GET vs POST dello stesso endpoint
await page.route('**/api/items', async route => {
  const request = route.request();

  if (request.method() === 'GET') {
    // GET: ritorna lista
    await route.fulfill({
      status: 200,
      body: JSON.stringify([
        { id: 1, name: 'Item 1' },
        { id: 2, name: 'Item 2' }
      ])
    });
  } else if (request.method() === 'POST') {
    // POST: ritorna l'oggetto creato con ID generato
    const bodyString = route.request().postData();
    const bodyObject = JSON.parse(bodyString || '{}');

    await route.fulfill({
      status: 201,
      body: JSON.stringify({
        id: Math.random().toString(36).substr(2, 9),
        ...bodyObject,
        createdAt: new Date().toISOString()
      })
    });
  } else if (request.method() === 'DELETE') {
    // DELETE: success 204 No Content
    await route.fulfill({ status: 204 });
  } else {
    // Metodo non supportato
    await route.fulfill({
      status: 405,
      body: JSON.stringify({ error: 'Method Not Allowed' })
    });
  }
});

3.2 Routing in base ai query parameter

// Snippet 5 - Mock diverso in base ai filtri della query string
await page.route('**/api/users**', async route => {
  const url = new URL(route.request().url());
  const role = url.searchParams.get('role');
  const limit = url.searchParams.get('limit') || '10';

  let users = [
    { id: 1, name: 'Alice', role: 'admin' },
    { id: 2, name: 'Bob', role: 'user' },
    { id: 3, name: 'Charlie', role: 'moderator' }
  ];

  if (role) {
    users = users.filter(u => u.role === role);
  }

  users = users.slice(0, parseInt(limit));

  await route.fulfill({
    status: 200,
    body: JSON.stringify(users)
  });
});

4. Intercettazione a livello di contesto vs pagina

Playwright permette di impostare rotte a due livelli: context (vale per tutte le pagine del contesto) e page (vale solo per una pagina specifica). Le rotte di page hanno priorità superiore, quindi se una rotta esiste sia a contesto che a pagina, la pagina vince.

// Snippet 6 - Context-level routes per riuso tra pagine
test.beforeEach(async ({ context }) => {
  // Questa rotta vale per TUTTE le pagine create in questo contesto
  await context.route('**/api/**', async route => {
    console.log('Context-level route intercettata:', route.request().url());
    await route.continue(); // passa al server reale
  });
});

test('pagina 1', async ({ page }) => {
  // Posso aggiungere ulteriori rotte page-specific
  await page.route('**/api/users', async route => {
    // Questa vince sulla rotta di context
    await route.fulfill({ status: 200, body: '[]' });
  });

  await page.goto('https://example.com/page1');
});

test('pagina 2', async ({ page }) => {
  // Usa la rotta di context (nessun override)
  await page.goto('https://example.com/page2');
});

5. Mocking di file binari, immagini e asset

Non tutte le risorse sono JSON. Playwright permette di mockare immagini, PDF, file binari, e altri asset.

5.1 Mock di un'immagine PNG

// Snippet 7 - Mock di immagine con body Buffer
import fs from 'fs';

test('mock immagine profilo', async ({ page }) => {
  // Leggi una piccola immagine PNG dal file system di test
  const pngBuffer = fs.readFileSync('./tests/fixtures/profile-pic.png');

  await page.route('**/profile.png', async route => {
    await route.fulfill({
      status: 200,
      contentType: 'image/png',
      body: pngBuffer
    });
  });

  await page.goto('https://example.com');
  const img = page.locator('img[alt="Profile"]');
  await expect(img).toBeVisible();
  // L'immagine viene servita dal mock, non dalla rete
});

6. Rate limiting e throttling simulato

A volte vuoi testare il comportamento del componente quando la rete è lenta o quando il server applica rate limiting. Playwright permette di simulare entrambi gli scenari all'interno di un mock.

// Snippet 8 - Rate limiting: rifiuta dopo N richieste
test('rate limiting', async ({ page }) => {
  let requestCount = 0;

  await page.route('**/api/search', async route => {
    requestCount++;

    if (requestCount > 3) {
      // Dopo 3 richieste, ritorna 429 Too Many Requests
      await route.fulfill({
        status: 429,
        headers: { 'Retry-After': '5' },
        body: JSON.stringify({ error: 'Rate limit exceeded' })
      });
    } else {
      // Altrimenti risposta normale
      await route.fulfill({
        status: 200,
        body: JSON.stringify({ results: ['item1', 'item2'] })
      });
    }
  });

  // Test qui: verifica che il componente gestisce 429 gracefully
});

// Snippet 9 - Throttling: aggiungi delay artificiale
await page.route('**/api/heavy-compute', async route => {
  // Simula che il server impiega 5 secondi a elaborare
  await page.waitForTimeout(5000);

  await route.fulfill({
    status: 200,
    body: JSON.stringify({ result: 'computed value' })
  });
});

7. Replay di risposte pre-registrate con HAR

Playwright supporta il formato HAR (HTTP Archive) per registrare e riprodurre risposte reali. Questo è utile quando vuoi testare con risposte reali dal server, ma offline.

// Snippet 10 - Registrazione di una HAR file
test.describe.configure({ mode: 'serial' });

test('registra le richieste in HAR', async ({ page }) => {
  // Playwright registra automaticamente TUTTI i network request/response
  // in un file HAR quando usi page.routeFromHAR()

  // Prima volta: registra
  await page.routeFromHAR('./tests/fixtures/app.har', {
    url: '**/api/**',
    update: 'missing' // registra solo richieste non già presenti
  });

  await page.goto('https://example.com');
  // Fai azioni che generano network request
  await page.click('button[data-action="fetch-data"]');
  // Tutte le risposte vengono salvate in app.har
});

test('usa la HAR registrata', async ({ page }) => {
  // Successivamente: riproduci offline senza contattare il server reale
  await page.routeFromHAR('./tests/fixtures/app.har', {
    url: '**/api/**'
  });

  await page.goto('https://example.com');
  // Le richieste /api/** vengono servite dal HAR file, non dal server
  // Test è deterministic e veloce
});

8. Best practice per interception robusta

Moccare la rete è un'arte. Ecco i pattern che mantengono i test leggibili e manutenibili nel tempo.

8.1 Fixture helpers per rotte comuni

// Snippet 11 - Estratto rotte comuni in fixture riusabili
export async function mockAuthAPI(page) {
  await page.route('**/api/auth/login', async route => {
    const body = JSON.parse(route.request().postData() || '{}');
    if (body.email === 'test@example.com' && body.password === 'correct') {
      await route.fulfill({
        status: 200,
        body: JSON.stringify({ token: 'mock-jwt-token-xyz' })
      });
    } else {
      await route.fulfill({
        status: 401,
        body: JSON.stringify({ error: 'Unauthorized' })
      });
    }
  });

  await page.route('**/api/auth/logout', async route => {
    await route.fulfill({ status: 204 });
  });
}

export async function mockProductsAPI(page, products = []) {
  await page.route('**/api/products', async route => {
    await route.fulfill({
      status: 200,
      body: JSON.stringify(products.length > 0 ? products : [
        { id: 1, name: 'Default Product', price: 99 }
      ])
    });
  });
}

// Uso nei test:
test('login flow', async ({ page }) => {
  await mockAuthAPI(page);
  await mockProductsAPI(page);

  await page.goto('https://example.com/login');
  // ... test login ...
});

9. Conclusione e punti chiave

Network interception in Playwright è la chiave per test affidabili e veloci. Riassumendo: usa page.route() per intercettare HTTP a livello di rotta, ispeziona request e response per implementare mocking dinamico, distingui tra context-level (per riuso) e page-level (per specifici test), simula errori HTTP, timeout e rate limiting, e considera HAR per replay di risposte reali.

Nel prossimo (e ultimo) articolo della serie affronteremo Accessibility Testing: come verificare che i componenti Playwright rispettino le linee guida WCAG, come usare i tool automatici di audit, e come scrivere test accessibilità che catturino problemi reali.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti