ITSkillsCenter
Développement Web

Tester SvelteKit 2 avec Vitest et Playwright : guide complet 2026

13 min de lecture

📍 Article principal du cluster : Svelte 5 et SvelteKit 2 en production : guide complet 2026

Introduction

L’équipe technique d’une fintech à Dakar livre une mise à jour critique un vendredi soir. Le lundi matin, plus aucun client ne peut valider de paiement Wave : un changement de schéma Zod a cassé silencieusement le formulaire principal. Le rollback prend quatre heures, l’équipe perd le week-end, et le CEO appelle le CTO pour expliquer la perte de revenu. Une suite de tests même modeste — Vitest sur les fonctions critiques, Playwright sur deux parcours utilisateur — aurait détecté la régression en 90 secondes en CI. Ce tutoriel pose les bases d’une stratégie de tests pragmatique pour SvelteKit 2 : assez pour dormir tranquille, pas trop pour ne pas freiner la livraison. À la fin, vous avez un projet typique avec 70 % de couverture sur la logique métier et 5-7 scénarios E2E qui valident les parcours qui rapportent de l’argent.

Prérequis

  • Projet SvelteKit 2 avec Svelte 5 fonctionnel
  • Node 22 LTS
  • Connaissance basique de la pyramide de tests (unitaire / intégration / E2E)
  • Niveau : intermédiaire — Temps : 2 h pour parcourir, 4 h pour pratiquer

Étape 1 — Stratégie : la pyramide adaptée

Avant d’écrire la moindre ligne de test, on choisit ce qu’on teste. La pyramide classique préconise beaucoup d’unitaires, quelques intégrations, peu d’E2E. Pour une app SaaS ouest-africaine de taille petite à moyenne, l’expérience montre qu’une pyramide adaptée fonctionne mieux : 30-40 % unitaires sur la logique métier pure (calculs, validation, transformation de données), 20-30 % intégration sur les form actions et endpoints, 10-15 % E2E sur les parcours qui génèrent du revenu (inscription, achat, paiement). Le reste du temps disponible va au refactoring et aux features, pas aux tests qui rassurent sans protéger.

Le principe directeur : un test n’a de valeur que s’il échoue quand le code casse vraiment. Un test qui ne fait que vérifier qu’un mock retourne ce qu’on a demandé au mock de retourner ne protège de rien. Tous les tests de cet article suivent la règle « tester le contrat, pas l’implémentation » : on vérifie le résultat observable côté utilisateur, pas la mécanique interne.

Étape 2 — Configurer Vitest

npm i -D vitest @testing-library/svelte @testing-library/jest-dom jsdom
// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  plugins: [sveltekit()],
  test: {
    environment: 'jsdom',
    setupFiles: ['./src/test-setup.ts'],
    include: ['src/**/*.{test,spec}.{js,ts}'],
    coverage: { provider: 'v8', reporter: ['text','html','json-summary'], exclude: ['**/*.svelte'] }
  }
});
// src/test-setup.ts
import '@testing-library/jest-dom/vitest';

Scripts dans package.json : "test": "vitest run", "test:watch": "vitest", "test:cov": "vitest run --coverage". Le mode watch est l’outil de TDD du quotidien : on lance npm run test:watch dans un terminal et chaque sauvegarde relance les tests pertinents en deux secondes.

Étape 3 — Tests unitaires de la logique métier

Pour la logique pure (validation Zod, calculs de prix, transformations de données), on écrit des tests sans Svelte ni jsdom. Ce sont les plus rapides et les plus robustes — ils ne cassent jamais à cause d’un changement d’UI.

// src/lib/calculs.ts
export function calculerTotal(items: Array<{prix:number;qte:number}>, tva = 0.18) {
  const ht = items.reduce((a,i) => a + i.prix * i.qte, 0);
  const remise = ht > 50000 ? ht * 0.05 : 0;
  return Math.round((ht - remise) * (1 + tva));
}
// src/lib/calculs.test.ts
import { describe, it, expect } from 'vitest';
import { calculerTotal } from './calculs';

describe('calculerTotal', () => {
  it('retourne 0 pour un panier vide', () => {
    expect(calculerTotal([])).toBe(0);
  });
  it('applique la TVA 18% par défaut', () => {
    expect(calculerTotal([{prix:1000,qte:1}])).toBe(1180);
  });
  it('applique la remise au-dessus de 50000 FCFA HT', () => {
    const r = calculerTotal([{prix:30000,qte:2}]);
    expect(r).toBe(Math.round(60000 * 0.95 * 1.18));
  });
});

Trois principes appliqués : nom de test descriptif (lisible comme une spec), un seul concept par test, valeurs d’entrée minimales pour isoler le comportement testé. Quand un test échoue, son nom doit suffire à comprendre ce qui ne va pas.

Étape 4 — Tests de composants Svelte

Pour les composants à logique non-triviale (formulaires, listes paginées, modales avec état), on les rend dans jsdom et on simule l’interaction utilisateur.

// src/lib/components/Compteur.test.ts
import { render, screen } from '@testing-library/svelte';
import { fireEvent } from '@testing-library/dom';
import { describe, it, expect } from 'vitest';
import Compteur from './Compteur.svelte';

describe('Compteur', () => {
  it('démarre à 0 par défaut', () => {
    render(Compteur);
    expect(screen.getByRole('button')).toHaveTextContent('0');
  });
  it('incrémente au clic', async () => {
    render(Compteur);
    const btn = screen.getByRole('button');
    await fireEvent.click(btn);
    await fireEvent.click(btn);
    expect(btn).toHaveTextContent('2');
  });
});

On évite de tester les détails d’implémentation interne (variables d’état, classes CSS internes). On vérifie seulement ce que l’utilisateur voit et fait. Cette discipline rend les tests stables face aux refactorings.

Étape 5 — Tests d’intégration des form actions et endpoints

Pour les form actions et les routes +server.ts, on teste la fonction exportée directement avec une Request construite à la main, sans démarrer un serveur HTTP. Plus rapide, plus déterministe.

// src/routes/api/clients/+server.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { GET, POST } from './+server';

describe('GET /api/clients', () => {
  it('retourne 401 sans cookie de session', async () => {
    const req = new Request('http://localhost/api/clients');
    const res = await GET({ request: req, cookies: { get: () => null } } as any);
    expect(res.status).toBe(401);
  });
});

Étape 6 — Configurer Playwright pour les E2E

npm init playwright@latest

Choisir TypeScript, dossier e2e, GitHub Actions oui. Modifier playwright.config.ts :

import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  retries: process.env.CI ? 2 : 0,
  use: { baseURL: 'http://localhost:4173', trace: 'on-first-retry' },
  webServer: { command: 'npm run build && npm run preview', port: 4173, reuseExistingServer: !process.env.CI },
  projects: [
    { name: 'chromium', use: devices['Desktop Chrome'] },
    { name: 'mobile', use: devices['Pixel 7'] }
  ]
});

Le projet « mobile » est crucial : il simule un Pixel 7 (proche d’un Tecno Camon haut de gamme) avec viewport étroit et touch events. Sans ce projet, on rate les bugs spécifiques au mobile, qui représentent 80 % du trafic ouest-africain.

Étape 7 — Scénarios E2E à valeur

On écrit cinq tests E2E maximum sur les parcours qui génèrent du revenu ou bloquent l’utilisation. Pas plus, pas moins. Pour un e-commerce typique : (1) parcours d’inscription complet, (2) ajout au panier puis checkout avec paiement Wave, (3) connexion utilisateur existant, (4) recherche produit et navigation, (5) consultation d’une commande passée. Chaque test doit être indépendant — pas de dépendance entre tests, pas d’ordre imposé.

// e2e/checkout.spec.ts
import { test, expect } from '@playwright/test';

test('checkout Wave : panier de 2 articles', async ({ page }) => {
  await page.goto('/');
  await page.getByRole('link', { name: 'Smartphones' }).click();
  await page.getByRole('button', { name: /ajouter/i }).first().click();
  await page.getByRole('link', { name: /panier/i }).click();
  await expect(page.getByText(/1 article/i)).toBeVisible();
  await page.getByRole('button', { name: /commander/i }).click();
  await page.getByLabel('Téléphone').fill('+221771234567');
  await page.getByLabel('Adresse').fill('Plateau, Dakar');
  await page.getByRole('radio', { name: /wave/i }).check();
  await page.getByRole('button', { name: /payer/i }).click();
  await expect(page).toHaveURL(/wave\.com|checkout\/success/);
});

Trois patterns à appliquer : utiliser getByRole et getByLabel plutôt que des sélecteurs CSS (les sélecteurs CSS cassent à chaque refactoring de style) ; ne jamais hardcoder un délai (page.waitForTimeout est le pire anti-pattern) ; toujours assertion finale qui valide que l’objectif fonctionnel est atteint.

Étape 8 — Factories et fixtures réutilisables

Pour éviter la duplication entre tests, on crée des fixtures Playwright qui pré-créent un utilisateur, un panier, ou un état de base.

// e2e/fixtures.ts
import { test as base } from '@playwright/test';

export const test = base.extend<{ userConnecte: { email:string; nom:string } }>({
  userConnecte: async ({ page }, use) => {
    await page.goto('/connexion');
    await page.getByLabel('Email').fill('test@itskills.sn');
    await page.getByLabel('Mot de passe').fill('TestPass2026!');
    await page.getByRole('button', { name: /connexion/i }).click();
    await page.waitForURL(/\/dashboard/);
    await use({ email: 'test@itskills.sn', nom: 'Aïssatou' });
  }
});

Côté tests, on utilise cette fixture : test('mon test', async ({ page, userConnecte }) => {...}). La connexion est gérée automatiquement, plus aucun copier-coller.

Étape 9 — Intégration CI GitHub Actions

name: Tests
on: [push, pull_request]
jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: npm }
      - run: npm ci
      - run: npm run test:cov
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: npm }
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: npx playwright test --project=chromium
      - uses: actions/upload-artifact@v4
        if: failure()
        with: { name: playwright-report, path: playwright-report/ }

Le job E2E télécharge le rapport en cas d’échec — précieux pour diagnostiquer un test cassé sans relancer en local. La couverture des tests unitaires se publie sur Codecov ou simplement en commentaire de PR via une action.

Erreurs fréquentes

Erreur Cause Solution
Tests passent local, échouent en CI Race condition liée à un timer ou une animation Remplacer waitForTimeout par waitForSelector ou toBeVisible
« jsdom not found » environment: 'jsdom' oublié dans config Vérifier vite.config.ts section test
Playwright : élément non visible CSS qui cache momentanément l’élément Utiliser .scrollIntoViewIfNeeded() avant clic
Tests d’intégration trop lents Connexion réelle à PostgreSQL Utiliser SQLite mémoire pour tests, PG en prod uniquement
Couverture incohérente Fichiers .svelte non instrumentés Configurer @sveltejs/svelte-coverage ou exclure
Test E2E stable au début, flaky avec le temps Données partagées entre tests Reset de la base avant chaque test, fixtures isolées

Adaptation au contexte ouest-africain

Trois ajustements pratiques. Premièrement, simuler les conditions réseau dégradées dans Playwright via page.route('**/*', route => ...) avec ajout de latence : tester ainsi qu’un parcours d’achat reste utilisable avec 1 seconde de latence par requête. Deuxièmement, valider l’accessibilité avec @axe-core/playwright sur chaque page importante — beaucoup d’utilisateurs ouest-africains accèdent via lecteur d’écran ou avec une vue diminuée. Troisièmement, pour les paiements mobile money, mocker l’API Wave/OM en E2E : le sandbox officiel ne supporte pas un volume CI suffisant, et on se concentre sur le comportement de l’app, pas sur la fiabilité de l’API tierce.

Côté budget temps, l’équipe doit prévoir 15 à 20 % du temps de dev pour les tests, surtout au démarrage. Cet investissement initial est rentabilisé dès la deuxième livraison où l’on évite le rollback en urgence un vendredi soir. Pour les structures qui démarrent et hésitent, commencer par les seuls tests E2E des deux parcours qui rapportent de l’argent — c’est 80 % de la valeur pour 20 % de l’effort.

Tutoriels frères

Pour aller plus loin

FAQ

Vitest ou Jest ?
Vitest. Il partage la config Vite avec SvelteKit, démarre en moins d’une seconde, et l’API est compatible Jest pour la migration. Jest reste valide mais sans avantage propre.

Playwright ou Cypress ?
Playwright. Multi-navigateur natif, plus rapide, meilleure ergonomie pour les iframes et popups. Cypress reste pertinent pour les équipes qui le maîtrisent déjà.

Faut-il tester chaque composant ?
Non. On teste les composants qui contiennent de la logique (formulaires, calculs, interactions). Les composants purement présentationnels n’ont besoin que d’un test E2E qui valide qu’ils s’affichent dans le contexte.

Comment gérer les tests qui dépendent d’une API externe ?
Mock systématique en CI via MSW (Mock Service Worker) ou par interception Playwright. Les tests réels contre l’API se lancent une fois par jour ou avant release, pas à chaque commit.

Tests de régression visuelle

Pour détecter les régressions CSS et de mise en page, Playwright propose un mécanisme natif de comparaison de captures. La première exécution génère une référence ; les suivantes la comparent pixel par pixel et signalent toute différence.

// e2e/visual.spec.ts
import { test, expect } from '@playwright/test';
test('homepage rendu identique', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot('homepage.png', { maxDiffPixels: 100 });
});

L’écueil classique : les différences de rendu de polices entre la machine du dev et le runner CI. Pour éviter les faux positifs, on utilise des polices web embarquées (pas de polices système), on lance les tests visuels uniquement en CI sur Linux, et on tolère une marge de quelques pixels avec maxDiffPixels. La régénération des références après un changement intentionnel se fait via npx playwright test --update-snapshots.

Pour un projet en production avec un design system stable, ces tests interceptent 80 % des régressions visuelles avant la prod. Pour un projet encore en évolution rapide de design, ils ralentissent — mieux vaut les activer plus tard.

Stratégie de données pour les tests

La qualité des tests dépend autant de leur design que des données qu’on leur fournit. Trois approches coexistent. La première, factories : on définit des fonctions creerUtilisateur(overrides), creerCommande(overrides) qui retournent des objets cohérents avec valeurs par défaut sensées. La seconde, fixtures statiques : un fichier JSON par scénario, lisible mais difficile à maintenir quand le schéma évolue. La troisième, data builders : pattern fluent userBuilder().avecRole('admin').avecPays('SN').build() — élégant pour les cas complexes mais sur-ingénierie pour 95 % des projets.

Notre recommandation : factories simples avec @faker-js/faker pour les valeurs aléatoires localisées (faker.locale = ‘fr’ fournit noms et adresses français). Pour les noms ouest-africains plausibles, on maintient un petit fichier noms-uemoa.ts avec 50 prénoms et 30 patronymes mélangés Sénégal/CI/Mali, qui rend les screenshots et logs de tests cohérents avec la cible utilisateur.

Tests de performance et budgets

Au-delà des tests fonctionnels, on définit des budgets de performance qui font échouer la CI si le bundle dépasse une taille ou si une page met trop de temps à charger. Pour SvelteKit, la lib size-limit ou un script personnalisé qui inspecte .svelte-kit/output font l’affaire. Budget typique pour une app B2B : 100 Ko gzippé sur la route /, 200 Ko cumulés sur n’importe quelle route. Au-delà, la PR ne passe pas la review automatique.

Côté Lighthouse, on intègre lhci qui lance Lighthouse en CI sur 3-5 URLs représentatives et publie un commentaire de PR avec les scores. Seuil minimal pour pages publiques : Performance > 85 sur Slow 4G, Accessibility > 95, Best Practices > 90. Ces seuils tirent le projet vers le haut et alertent au moindre dérapage.

Besoin d'un site web ?

Confiez-nous la Création de Votre Site Web

Site vitrine, e-commerce ou application web — nous transformons votre vision en réalité digitale. Accompagnement personnalisé de A à Z.

À partir de 250.000 FCFA
Parlons de Votre Projet
Publicité