J 3 — Tester avec in-memory

L'InMemoryRepository : un outil de test puissant

Bootcode IWA-S04 — Semaine 15, Jour 3

Objectifs de la leçon

1. Couvrir tous les chemins d'exécution

Succès ET échec dans chaque test

2. Tester les cas d'erreur avec toThrow()

Vérifier que les erreurs sont levées

3. Pré-remplir un InMemoryRepository

Tester des scénarios avec données existantes

4. Identifier et tester les cas limites

Edge cases : vide, doublons, valeurs extrêmes

Plan du cours

1

FLASHBACK : l'InMemoryRepository de S10

C'était un OUTIL DE TEST !

2

Deux implémentations, une interface

PostgreSQL en prod, InMemory en test

3

Injection de dépendances

Vous le faites depuis S11 !

4

Test complet avec InMemoryRepository

Créer, vérifier, erreurs

5

Pré-remplir le repository

Tester des scénarios existants

Module 1

FLASHBACK : S10

Vous avez créé un InMemoryRepository — et c'était un OUTIL DE TEST !

Rappelez-vous... Semaine 10

Vous avez créé une classe qui stocke les données dans un Map en mémoire

class InMemoryStudentRepository implements StudentRepository {

private students = new Map<string, Student>();

save(student: Student): void {

this.students.set(student.id, student);

}

findById(id: string): Student {

return this.students.get(id);

}

}

💡 Aujourd'hui, cet outil prend tout son sens !

On l'utilise pour tester la logique métier SANS base de données

Pourquoi un InMemoryRepository ?

🐌

Tester avec PostgreSQL

  • • Il faut une base de données qui tourne
  • • Les tests sont lents (I/O disque)
  • • Les données se polluent entre les tests
  • • Configuration complexe en CI/CD

Tester avec InMemory

  • • Aucune base de données nécessaire
  • • Les tests sont instantanés (RAM)
  • • beforeEach recrée un repo vide
  • • Ça marche partout, sans config

🎯 On teste la logique métier, pas la base de données

Module 2

Deux implémentations, une interface

PostgreSQL en prod · InMemory en test

Le contrat : l'interface

L'interface définit les méthodes. Les implémentations les réalisent différemment.

interface StudentRepository {

save(student: Student): Promise<void>;

findById(id: string): Promise<Student | null>;

findAll(): Promise<Student[]>;

delete(id: string): Promise<void>;

}

🔑 Le service ne connaît que l'interface

Il ne sait PAS quelle implémentation il utilise — c'est ça la magie !

Deux implémentations, même contrat

🏭 Production

class PgStudentRepository

implements StudentRepository {

save(s) {

// INSERT INTO students...

await this.pool.query(...)

}

}

🧪 Test

class InMemoryStudentRepo

implements StudentRepository {

save(s) {

// this.students.set(s.id, s)

this.students.set(s.id, s)

}

}

✅ Mêmes méthodes, même signature → le service ne voit AUCUNE différence

Module 3

Injection de dépendances

Vous le faites depuis S11 !

Le concept en une phrase

Le service ne crée PAS son repository — il le REÇOIT

❌ Le service crée sa dépendance

class EnrollStudent {

private repo = new PgStudentRepo();

// Coincé avec PostgreSQL !

}

✅ Le service reçoit sa dépendance

class EnrollStudent {

constructor(private repo: StudentRepository) {}

// On peut injecter InMemory !

}

🔑 Le type du paramètre est l'interface, pas une classe concrète

En production vs en test

// Le service dépend de l'INTERFACE

class EnrollStudent {

constructor(private repo: StudentRepository) {}

async execute(studentData) {

const student = new Student(studentData);

await this.repo.save(student);

return student;

}

}

🏭 Production

const repo = new PgStudentRepo(pool);

const useCase = new EnrollStudent(repo);

🧪 Test

const repo = new InMemoryStudentRepo();

const useCase = new EnrollStudent(repo);

✅ Même use case, même code — seul le repository change. C'est l'injection de dépendances !

Module 4

Test complet avec InMemoryRepository

Créer, vérifier, erreurs

Le workflow de test

1️⃣

Créer un InMemoryRepository vide

Dans beforeEach — un repo frais pour chaque test

2️⃣

Injecter le repo dans le use case

new EnrollStudent(inMemoryRepo)

3️⃣

Appeler le use case et vérifier le résultat

expect(result) / expect(repo.findById) / toThrow()

beforeEach(() => {

repo = new InMemoryStudentRepo();

useCase = new EnrollStudent(repo);

});

Test 1 : le chemin succès ✅

On crée un étudiant, puis on vérifie qu'il est bien stocké

it('enregistre un nouvel étudiant', async () => {

// 1. On appelle le use case

const student = await useCase.execute({

nom: 'Alice',

email: '[email protected]'

});

// 2. On vérifie le résultat retourné

expect(student.nom).toBe('Alice');

expect(student.id).toBeDefined();

// 3. On vérifie qu'il est BIEN dans le repo

const found = await repo.findById(student.id);

expect(found).toEqual(student);

});

💡 On vérifie DEUX choses : le retour du use case ET l'état du repository

Test 2 : le chemin erreur avec toThrow() ❌

On teste que le use case LÈVE une erreur quand les données sont invalides

it('lève une erreur si email déjà utilisé', async () => {

// Pré-remplir avec un étudiant existant

await repo.save(new Student({

nom: 'Bob', email: '[email protected]'

}));

// Tenter d'enregistrer un doublon

await expect(useCase.execute({

nom: 'Alice', email: '[email protected]'

})).rejects.toThrow('Email déjà utilisé');

});

⚠️ Pour les fonctions async, utiliser rejects.toThrow()

Pas juste toThrow() — les Promises rejettent, ne throw pas directement

beforeEach : chaque test est indépendant 🔄

beforeEach recrée un repo VIDE avant chaque test — pas de pollution entre tests

let repo: InMemoryStudentRepo;

let useCase: EnrollStudent;

beforeEach(() => {

// Avant CHAQUE test : un repo tout neuf

repo = new InMemoryStudentRepo();

useCase = new EnrollStudent(repo);

});

it('test 1', ...) // repo vide

it('test 2', ...) // repo vide aussi !

❌ Sans beforeEach

Le test 2 voit les données du test 1 → résultats imprévisibles

✅ Avec beforeEach

Chaque test démarre à zéro → reproductible et fiable

Couvrir TOUS les chemins d'exécution 🛣️

Un use case n'a pas qu'un seul chemin — il faut tester chaque branche

✅ Chemin succès

  • • Données valides → étudiant créé
  • • Email unique → pas d'erreur
  • • Vérifier le retour ET le repo

❌ Chemins erreur

  • • Email déjà utilisé → toThrow()
  • • Nom vide → toThrow()
  • • Email invalide → toThrow()

// Un test par chemin = couverture complète

it('succès avec données valides', ...)

it('erreur si email dupliqué', ...)

it('erreur si nom vide', ...)

it('erreur si email invalide', ...)

Module 5

Pré-remplir le repository

Tester des scénarios avec données existantes

Pourquoi pré-remplir le repo ? 📦

Certains scénarios nécessitent des données EXISTANTES pour être testés

🔍

Tester "trouver un étudiant qui existe"

Il faut d'abord mettre un étudiant dans le repo

🚫

Tester "email déjà utilisé"

Il faut un étudiant avec cet email AVANT le test

📋

Tester "lister tous les étudiants"

Il faut plusieurs étudiants pour vérifier le compte

Comment pré-remplir : le code

On utilise repo.save() AVANT d'appeler le use case — directement sur le repo

it('lève une erreur si email déjà utilisé', async () => {

// 1. Pré-remplir : un étudiant existe déjà

const existing = new Student({

id: '123',

nom: 'Bob',

email: '[email protected]'

});

await repo.save(existing);

// 2. Maintenant on teste le doublon

await expect(useCase.execute({

nom: 'Alice', email: '[email protected]'

})).rejects.toThrow('Email déjà utilisé');

});

💡 On injecte directement dans le repo — on ne passe PAS par le use case pour pré-remplir

Identifier et tester les cas limites 🎯

Les edge cases : c'est là que les bugs se cachent !

📋 Repo vide

findAll() retourne [] — pas undefined ni null

🔍 ID inexistant

findById('xyz') retourne null, pas une erreur

📝 Nom très long

255 caractères — ça passe ou ça casse ?

🗑️ Supprimer un ID inexistant

Erreur silencieuse ou exception ?

it('findAll retourne un tableau vide si repo vide', async () => {

const all = await repo.findAll();

expect(all).toEqual([]);

});

⚠️ Pièges courants à éviter

❌ Oublier beforeEach

// Pas de beforeEach → pollution !

let repo = new InMemoryRepo();

it('test 1', () => {

repo.save(student);

});

it('test 2', () => {

// repo a ENCORE le student du test 1 !

});

✅ Toujours utiliser beforeEach

let repo: InMemoryRepo;

beforeEach(() => {

repo = new InMemoryRepo();

});

it('test 2', () => {

// repo vide, comme il se doit !

});

⚠️ Piège : tester la MAUVAISE chose

❌ Tester l'InMemoryRepo lui-même

it('save ajoute au Map', () => {

repo.save(student);

expect(repo.students.size).toBe(1);

});

// On teste l'outil, pas la logique !

✅ Tester le use case via le repo

it('enroll crée un étudiant', () => {

const result = useCase.execute(data);

expect(result.nom).toBe(data.nom);

});

// On teste la LOGIQUE MÉTIER !

🎯 L'InMemoryRepository est un OUTIL, pas le sujet du test

On teste le use case. Le repo est juste là pour simuler le stockage.

⚠️ Étudiants qui ont sauté les interfaces en S10

Si vous n'avez pas créé d'interface en S10...

L'injection de dépendances ne marche PAS sans interface !

Le use case ne peut pas accepter "n'importe quel repo" s'il n'y a pas de contrat commun.

🔧 Action : rattraper individuellement

  • • Créer l'interface StudentRepository maintenant
  • • Faire implémenter l'interface par InMemoryStudentRepo
  • • Changer le type du paramètre dans le use case
  • • Demander de l'aide au formateur si besoin !

À retenir !

🧪

InMemory = outil de test

Pas de base de données, tests instantanés

🔌

Injection de dépendances

Le service reçoit son repo, ne le crée pas

🔄

beforeEach = indépendance

Un repo vide avant chaque test

🎯

Tester la logique, pas l'outil

Le use case est le sujet, le repo est le simulateur

Questions ?

L'InMemoryRepository : le pont entre S10 et aujourd'hui

Prochaine étape : pratiquer l'injection de dépendances sur vos propres use cases