Changer le thème

Tests unitaires Vitest : de la configuration au workflow TDD

Easton editorial illustration: one large test card moving through a three-stage TDD loop

Combien de temps faut-il pour configurer Jest sur un projet ESM ? ts-jest, babel-jest, jest.config.js… et toute une série de problèmes de résolution de modules. J’ai déjà passé un après-midi entier rien que pour que Jest reconnaisse correctement les import de fichiers .vue.

Avec Vitest, une ligne de configuration suffit.

Ce n’est pas une exagération. La première fois que j’ai lancé vitest dans un projet Vite, la suite s’est exécutée en quelques secondes — comme remplacer un PC lent de trois ans : tout devient fluide.

Vitest est-il vraiment si rapide ? Les données officielles et les benchmarks communautaires convergent : démarrage à froid ~200 ms (Jest : 2-4 s), ~500 cas de test en ~8 s (Jest : ~45 s). Il partage la config Vite, supporte TypeScript nativement, et l’API ressemble à Jest — la migration peut tenir en une demi-heure.

Cet article vous mène de la configuration initiale au workflow TDD complet, avec mocking et couverture. Nouveau projet ou migration depuis Jest, vous y trouverez ce qu’il vous faut.

Qu’est-ce que Vitest ? Pourquoi est-il si rapide ?

En bref, Vitest est le framework de tests natif de Vite.

Si vous construisez déjà avec Vite, Vitest est prêt à l’emploi. Il réutilise la config Vite — alias, variables d’environnement, traitement CSS — sans empiler transform, moduleFileExtensions et moduleNameMapper comme avec Jest.

Trois atouts principaux :

Vitesse. Démarrage à froid ~200 ms ; Jest met souvent 2-4 s. Sur un gros projet : ~500 tests en ~8 s (Vitest) contre ~45 s (Jest), d’après des mesures DEV Community en 2026. L’écart est net.

Compatibilité Jest. API quasi identique — describe, it, expect, vi.fn() — la migration se résume souvent à changer les chemins d’import.

Watch intelligent. La fonction « HMR for tests » ne relance que les tests impactés, pas toute la suite. Le retour immédiat en développement change la donne.

Passons à l’installation.

Installation et configuration

Prérequis : Vite >= 6.0.0, Node >= 20.0.0. Sur un projet récent, c’est en général déjà le cas.

Installation

Une commande :

npm install -D vitest

C’est tout. Pas besoin de @types/jest, ts-jest, jest-environment-jsdom… Vitest gère TypeScript nativement.

Fichier de configuration

Deux options : ajouter test dans vite.config.ts, ou créer un vitest.config.ts dédié.

Pour un projet simple, vite.config.ts suffit :

// vite.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    globals: true,  // describe, it, expect globaux sans import
    environment: 'node', // ou 'jsdom' pour le navigateur
    include: ['tests/**/*.test.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov'],
    },
  },
})

globals: true est pratique : describe, it, expect deviennent globaux, comme avec Jest.

Pour les API DOM (composants), passez environment à 'jsdom' et installez jsdom :

npm install -D jsdom

Scripts npm

Dans package.json :

{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run"
  }
}

npm test lance le watch ; npm run test:run une exécution unique (CI).

La config tient en peu de lignes — bien moins que les preset, transform et moduleFileExtensions de Jest.

Écrire des tests unitaires

Convention : .test.ts ou .spec.ts, dans tests/ ou à côté des sources — selon l’équipe.

Structure de base

Un test minimal :

import { describe, it, expect } from 'vitest'
import { add, divide } from './math'

describe('Math utilities', () => {
  it('should add two numbers', () => {
    expect(add(2, 3)).toBe(5)
  })

  it('should throw on division by zero', () => {
    expect(() => divide(10, 0)).toThrow('Division by zero')
  })
})

describe groupe les tests, it définit un cas, expect assert.

Avec globals: true, l’import peut être omis.

Assertions courantes

Les plus utilisées :

// Égalité
expect(value).toBe(5)            // stricte (===)
expect(obj).toEqual({ a: 1 })    // profonde

// Vérité
expect(value).toBeTruthy()
expect(value).toBeFalsy()
expect(value).toBeNull()

// Exceptions
expect(() => fn()).toThrow()
expect(() => fn()).toThrow('Error message')

// Nombres
expect(n).toBeGreaterThan(10)
expect(n).toBeLessThanOrEqual(5)

// Tableaux / chaînes
expect(arr).toContain('item')
expect(str).toMatch(/pattern/)

Filtrer les tests

Pour n’exécuter qu’un test : .only :

it.only('n\'exécute que celui-ci', () => { ... })

Pour ignorer temporairement : .skip :

it.skip('ignoré pour l\'instant', () => { ... })

Même chose pour describe : describe.only(...), describe.skip(...).

En mode watch, modifier le code fait passer les tests au vert ou au rouge immédiatement — bien plus agréable que d’attendre plusieurs secondes à chaque run Jest.

TDD en pratique

Le TDD (test-driven development), c’est simple : tests d’abord, code ensuite.

Ça paraît contre-intuitif, mais ça force à clarifier ce que la fonction doit faire avant d’implémenter — plutôt que d’ajouter des tests après coup pour « faire le quota » de couverture.

Exemple complet : une fonction validateEmail.

Étape 1 : tests sans implémentation

Créez le fichier de test :

// tests/validateEmail.test.ts
import { describe, it, expect } from 'vitest'
import { validateEmail } from '../src/validateEmail'

describe('validateEmail', () => {
  it('should return true for valid email', () => {
    expect(validateEmail('[email protected]')).toBe(true)
  })

  it('should return false for invalid email', () => {
    expect(validateEmail('invalid')).toBe(false)
  })
})

La fonction n’existe pas encore : les tests échouent. C’est la première étape du TDD.

Étape 2 : code minimal

Implémentation minimale pour passer :

// src/validateEmail.ts
export function validateEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}

Lancez npm test. Les deux tests passent ? Étape 2 terminée.

Étape 3 : tests de bord

Ajoutez des cas limites :

// tests/validateEmail.test.ts (suite)
it('should return false for empty string', () => {
  expect(validateEmail('')).toBe(false)
})

it('should return false for email without domain', () => {
  expect(validateEmail('user@')).toBe(false)
})

it('should return false for email with spaces', () => {
  expect(validateEmail('test @example.com')).toBe(false)
})

Si tout passe, la regex tient la route ; sinon, corrigez-la.

Étape 4 : refactor

Les tests passent : vous pouvez refactoriser — regex plus stricte, constantes, etc. :

// src/validateEmail.ts
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/

export function validateEmail(email: string): boolean {
  if (!email || email.trim() === '') {
    return false
  }
  return EMAIL_REGEX.test(email)
}

Relancez les tests : toujours verts. Le TDD protège le refactor.

Pourquoi le TDD aide

Au début, « tests d’abord » déroute. Après quelques cycles :

  1. Réfléchir avant de coder : le test décrit le comportement attendu.
  2. Itération rapide : le watch Vitest répond en secondes.
  3. Refactor en confiance : la suite signale toute régression.

Commencez par des utilitaires simples (formatage, helpers), puis montez en complexité.

Mocking avancé

Les tests unitaires isolent souvent les dépendances — API, base de données, libs tierces. D’où les mocks.

Vitest propose une API proche de Jest, centrée sur vi.

vi.fn() : mocker une fonction

Créer une fausse fonction :

import { vi, describe, it, expect } from 'vitest'

describe('vi.fn() demo', () => {
  it('tracks calls', () => {
    const mockFn = vi.fn()

    mockFn('hello')
    mockFn('world')

    expect(mockFn).toHaveBeenCalledTimes(2)
    expect(mockFn).toHaveBeenNthCalledWith(1, 'hello')
  })
})

Valeur de retour prédéfinie :

const mockFn = vi.fn().mockReturnValue('mocked result')
// ou asynchrone
const asyncMock = vi.fn().mockResolvedValue({ data: 'ok' })

vi.mock() : mocker un module

Pour une fonction qui appelle une API externe :

import { vi, describe, it, expect, beforeEach } from 'vitest'
import { fetchUser } from './api'
import { UserService } from './UserService'

vi.mock('./api', () => ({
  fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Alice' })
}))

describe('UserService', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('should fetch user', async () => {
    const service = new UserService()
    const user = await service.getUser(1)

    expect(user.name).toBe('Alice')
    expect(fetchUser).toHaveBeenCalledWith(1)
  })
})

vi.mock() s’exécute avant l’import du module — placez-le en tête de fichier.

vi.spyOn() : espionner sans remplacer

Pour enregistrer les appels sans casser l’implémentation :

import { vi, describe, it, expect, afterEach } from 'vitest'
import { calculator } from './calculator'

describe('spyOn demo', () => {
  afterEach(() => {
    vi.restoreAllMocks()
  })

  it('tracks add calls', () => {
    const addSpy = vi.spyOn(calculator, 'add')

    const result = calculator.add(2, 3)

    expect(result).toBe(5)
    expect(addSpy).toHaveBeenCalledWith(2, 3)
  })
})

spyOn est plus doux que mock : la fonction réelle tourne, avec un « enregistreur » en plus.

Mocker les objets globaux

Sans vraie requête HTTP :

vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
  ok: true,
  json: () => Promise.resolve({ data: 'mocked' })
}))

vi.stubGlobal s’applique aussi à window, localStorage, etc.

Le mocking est la partie la plus délicate. Commencez par vi.fn(), puis vi.mock(). Nettoyez entre les tests (clearAllMocks ou restoreAllMocks) pour éviter la pollution entre cas.

Couverture et bonnes pratiques

La couverture mesure une partie de la qualité. Vitest propose v8 (rapide, natif) et istanbul (compatibilité). En pratique, v8 suffit.

Configurer la couverture

Dans vite.config.ts :

test: {
  coverage: {
    provider: 'v8',
    reporter: ['text', 'html', 'lcov'],
    thresholds: {
      lines: 80,
      functions: 80,
      branches: 70,
    },
    exclude: ['node_modules/', 'tests/', '**/*.d.ts'],
  },
}

Commande :

vitest run --coverage

Le terminal affiche le rapport ; le dossier coverage/ contient la version HTML des lignes non couvertes.

Intérêt des seuils

Si la couverture est sous le seuil, Vitest échoue — utile en CI pour bloquer les commits trop légers. 80 % est un bon point de départ ; viser 100 % épuise souvent l’équipe.

Intégration CI/CD

Dans GitHub Actions ou autre CI :

# .github/workflows/test.yml
- name: Run tests with coverage
  run: npm run test:run -- --coverage

Uploadez le rapport lcov vers Codecov ou Coveralls pour suivre l’évolution.

Quelques conseils

  1. Pas de quête du 100 % : un chiffre élevé ne garantit pas la qualité. Testez la logique critique.
  2. Chemins principaux d’abord : happy path prioritaire, branches d’erreur ensuite.
  3. Nettoyer les tests obsolètes : la suite se maintient comme le code.
  4. Garder le watch actif : vitest en écoute pendant le dev pour détecter tout de suite.

Synthèse

Vitest, en trois mots : rapide, simple, efficace.

Rapide — ~200 ms à froid, ~500 tests en ~8 s, un ordre de grandeur devant Jest. Simple — config partagée avec Vite, peu de réglages. Efficace — API proche de Jest, migration légère.

Projet Vite ? Vitest est le choix naturel ; inutile de se battre avec l’ESM sur Jest.

Encore sur Jest ? Essayez une migration en une demi-heure : installez Vitest, changez les imports — souvent ça tourne directement.

Pour le TDD, commencez petit : utilitaires, tests d’abord. Vous verrez que « réfléchir avant de coder » accélère souvent le travail.

Les tests ne sont pas une corvée, c’est une assurance. Configurez Vitest correctement, et vous coderez plus sereinement.

Configuration Vitest et workflow TDD

Étapes complètes pour configurer Vitest et pratiquer le développement piloté par les tests

⏱️ Estimated time: 30 min

  1. 1

    Step 1: Installer Vitest

    Exécutez la commande d'installation :

    ```bash
    npm install -D vitest
    ```

    Prérequis : Vite >= 6.0.0, Node >= 20.0.0
  2. 2

    Step 2: Configurer vite.config.ts

    Ajoutez le champ test dans la configuration :

    ```typescript
    import { defineConfig } from 'vitest/config'

    export default defineConfig({
    test: {
    globals: true,
    environment: 'node',
    include: ['tests/**/*.test.ts'],
    },
    })
    ```

    globals: true évite d'importer describe, it et expect à chaque fichier.
  3. 3

    Step 3: Ajouter les scripts npm

    Dans package.json :

    ```json
    {
    "scripts": {
    "test": "vitest",
    "test:run": "vitest run"
    }
    }
    ```

    npm test lance le mode watch ; npm run test:run une exécution unique (CI).
  4. 4

    Step 4: Écrire le premier test

    Créez tests/math.test.ts :

    ```typescript
    import { describe, it, expect } from 'vitest'

    describe('Math', () => {
    it('should add numbers', () => {
    expect(1 + 1).toBe(2)
    })
    })
    ```

    Lancez npm test pour valider la configuration.
  5. 5

    Step 5: Pratiquer le workflow TDD

    Suivez le développement piloté par les tests :

    • Étape 1 : écrire le test et définir le comportement attendu
    • Étape 2 : implémenter le minimum pour passer
    • Étape 3 : ajouter les tests de bord
    • Étape 4 : refactoriser

    Le mode watch Vitest offre un retour en quelques secondes.
  6. 6

    Step 6: Configurer la couverture

    Ajoutez la configuration coverage :

    ```typescript
    coverage: {
    provider: 'v8',
    reporter: ['text', 'html'],
    thresholds: {
    lines: 80,
    functions: 80,
    },
    }
    ```

    Exécutez vitest run --coverage pour générer le rapport.

FAQ

Quelle différence entre Vitest et Jest ?
Vitest est le framework de tests natif de Vite : il partage la configuration, démarre à froid en ~200 ms (Jest : 2-4 s). L'API est quasi identique à Jest, la migration est légère. Avantages : vitesse, configuration simple, TypeScript natif.
Comment migrer de Jest vers Vitest ?
Migration en quelques étapes :

• Désinstallez les paquets Jest, installez vitest
• Migrez jest.config.js vers vite.config.ts
• Remplacez import from 'jest' par from 'vitest'
• Remplacez jest.fn() et jest.mock() par vi.fn() et vi.mock()

En général, 30 minutes suffisent.
Quels environnements de test Vitest prend-il en charge ?
Plusieurs environnements : node (défaut, backend), jsdom (DOM navigateur), happy-dom (alternative DOM plus rapide). Définissez environment dans la config ; pour les composants navigateur, installez jsdom.
Comment mocker les requêtes API dans Vitest ?
Trois approches courantes :

• vi.fn() : mocker une fonction avec une valeur de retour prédéfinie
• vi.mock() : mocker un module entier et remplacer ses exports
• vi.spyOn() : surveiller les appels sans remplacer l'implémentation

Nettoyez avec vi.clearAllMocks() ou vi.restoreAllMocks() après chaque test.
Comment configurer la couverture Vitest ?
Dans test.coverage de vite.config.ts : provider (v8 recommandé), reporter (text/html/lcov) et thresholds. Lancez vitest run --coverage. Si les seuils ne sont pas atteints, Vitest échoue — pratique en CI pour imposer la qualité.
Quel est le cœur du workflow TDD ?
Le TDD repose sur la boucle rouge-vert-refactor :

• Rouge : écrire d'abord un test qui échoue
• Vert : écrire le minimum de code pour passer
• Refactor : améliorer la structure

Avec le mode watch Vitest, le retour est quasi instantané. Écrire les tests en premier clarifie la conception des fonctions.

8 min de lecture · Publié le: 14 avr. 2026 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog