Tests en pratique - 07 - Organiser ses tests dans un projet

Co-location vs dossier __tests__, nommage, séparation par type, scripts et configuration bun test.

07 - Organiser ses tests dans un projet

Ce que tu vas apprendre

  • Nommer ses fichiers de test de facon coherente
  • Co-location vs dossier séparé : quand choisir quoi
  • Separer unit, intégration et e2e dans la CI
  • Les factories et helpers pour ne pas se répéter

Prerequisites

Avoir des tests qui tournent avec bun test. Avoir lu les articles précédents sur les types de tests.


Le projet ou personne ne retrouve les tests

Un de mes juniors sur paltemps.fr avait créé un dossier tests/ a la racine, avec 30 fichiers plats. test1.ts, test2.ts, testUsers.ts, testMission.ts, testMissionV2.ts. Aucune convention de nommage, aucune structure, aucun moyen de savoir quel test correspondait a quel module.

On a passe une apres-midi a tout reorganiser. Depuis, on a des regles claires. Ca prend 5 minutes a apprendre et ca évité des heures de confusion.

Convention de nommage

La convention Bun (et Jest, et Vitest) : .test.ts. Le nom du fichier de test reprend le nom du fichier source :

order.ts           -> order.test.ts
user-service.ts    -> user-service.test.ts
pricing.ts         -> pricing.test.ts

Pour distinguer les types de tests, on ajoute un suffixe :

order.test.ts                  # unit test
order.integration.test.ts      # integration (DB, HTTP)
order.e2e.test.ts              # end-to-end

C'est une convention, pas une obligation technique. Bun s'en fiche du nom. Mais ca permet de filtrer :

bash# Lancer seulement les tests unitaires
bun test --filter '*.test.ts' --exclude '*.integration.*' --exclude '*.e2e.*'

# Lancer seulement les tests d'integration
bun test --filter '*.integration.test.ts'

Co-location vs dossier séparé

Deux ecoles. La co-location : le test est a cote du fichier source. Le dossier séparé : tous les tests dans tests/ ou __tests__/.

Mon avis : ca depend du type de test.

Co-location pour les tests unitaires. Le test est intimement lie au fichier source. Quand tu ouvres order.ts, tu vois immédiatement order.test.ts juste a cote. Tu sais qu'il existe. Tu le mets à jour en meme temps que le code.

src/
  domain/
    order.ts
    order.test.ts
    pricing.ts
    pricing.test.ts
  infrastructure/
    db.ts
    db.test.ts

Dossier séparé pour intégration et e2e. Ces tests ne sont pas lies a un seul fichier. Un test d'intégration peut toucher la base, le mail, le fichier system. Un test e2e traverse tout le projet. Les mettre a cote d'un fichier source n'a pas de sens.

src/
  domain/
    order.ts
    order.test.ts
tests/
  integration/
    order-repo.integration.test.ts
    mail-service.integration.test.ts
  e2e/
    login-flow.e2e.test.ts
    mission-creation.e2e.test.ts
  helpers/
    factories.ts
    setup.ts

C'est la structure qu'on utilise sur paltemps.fr. Pas parfaite, mais elle répond a la question "ou est le test de X ?" en moins de 2 secondes.

Scripts dans package.json

Des scripts clairs pour chaque type de test. Pas de commande magique que seul le lead dev connaît :

json{
  "scripts": {
    "test": "bun test",
    "test:unit": "bun test src/ --exclude '*.integration.*' --exclude '*.e2e.*'",
    "test:integration": "bun test tests/integration/",
    "test:e2e": "bunx playwright test",
    "test:coverage": "bun test --coverage",
    "test:watch": "bun test --watch"
  }
}

bun test tout court lance tout. C'est le défaut, c'est ce que la CI utilise. Les scripts spécifiques servent au dev local quand tu bosses sur un type de test particulier.

Le --watch est un gain de temps énorme en dev. Le test se relance a chaque sauvegarde. Je l'utilise tout le temps quand je fais du TDD (voir l'article sur le TDD).

Les helpers et factories

Quand tu as 50 tests qui creent un utilisateur, tu ne veux pas répéter le meme code 50 fois. Les factories centralisent la création d'objets de test :

typescript// tests/helpers/factories.ts

import type { Order } from "../../src/domain/order";
import type { User } from "../../src/domain/user";

export function createTestUser(overrides?: Partial<User>): User {
  return {
    id: crypto.randomUUID(),
    email: "test@test.com",
    name: "Test User",
    role: "user",
    createdAt: new Date("2026-01-01"),
    ...overrides,
  };
}

export function createTestOrder(overrides?: Partial<Order>): Order {
  return {
    id: crypto.randomUUID(),
    items: [{ product: "Widget", qty: 1, price: 10 }],
    customerEmail: "test@test.com",
    status: "pending",
    createdAt: new Date("2026-01-01"),
    ...overrides,
  };
}

L'usage dans les tests :

typescriptimport { createTestUser, createTestOrder } from "../helpers/factories";

it("assigns order to user", () => {
  const user = createTestUser({ role: "admin" });
  const order = createTestOrder({ customerEmail: user.email });

  const result = assignOrder(order, user);
  expect(result.assignedTo).toBe(user.id);
});

Le overrides en paramètre optionnel, c'est ce qui rend les factories flexibles. Tu reutilises les valeurs par défaut sauf pour les champs qui comptent dans TON test. Le test ci-dessus ne s'interesse qu'au rôle et a l'email. Tout le reste ce sont des valeurs par défaut dont on se fiche.

Le fichier de setup

Pour les tests d'intégration, tu as souvent besoin d'initialiser la base, de nettoyer entre les tests, de configurer un serveur SMTP de test. Un fichier de setup centralise ca :

typescript// tests/helpers/setup.ts

import { db } from "../../src/infrastructure/db";

export async function cleanDatabase() {
  await db.delete(missions);
  await db.delete(users);
  // Ordre important : respecter les foreign keys
}

export async function seedTestData() {
  await db.insert(users).values([
    { id: "user-1", email: "admin@test.com", role: "admin" },
    { id: "user-2", email: "user@test.com", role: "user" },
  ]);
}
typescript// tests/integration/mission-repo.integration.test.ts

import { cleanDatabase, seedTestData } from "../helpers/setup";
import { beforeEach } from "bun:test";

beforeEach(async () => {
  await cleanDatabase();
  await seedTestData();
});

L'arbre final

Pour un projet de la taille de paltemps.fr, voici la structure complète :

api/
  src/
    domain/
      order.ts
      order.test.ts
      mission.ts
      mission.test.ts
    application/
      create-mission.ts
      create-mission.test.ts
    infrastructure/
      mission-repo.ts
      mail-service.ts
  tests/
    integration/
      mission-repo.integration.test.ts
      mail-service.integration.test.ts
    e2e/
      login.e2e.test.ts
      mission-crud.e2e.test.ts
    helpers/
      factories.ts
      setup.ts
  package.json
  bunfig.toml

Pas de dossier __tests__ a la racine. Pas de fichiers test1.ts. Chaque test dit ce qu'il teste par son nom et son emplacement. Un nouveau dev qui arrive sur le projet comprend la structure en 30 secondes.


Article précédent : 06 - Tester du legacy sans tout casser

Article suivant : 08 - Glossaire général des tests

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.