L'InMemoryRepository : un outil de test puissant
Bootcode IWA-S04 — Semaine 15, Jour 3
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
FLASHBACK : l'InMemoryRepository de S10
C'était un OUTIL DE TEST !
Deux implémentations, une interface
PostgreSQL en prod, InMemory en test
Injection de dépendances
Vous le faites depuis S11 !
Test complet avec InMemoryRepository
Créer, vérifier, erreurs
Pré-remplir le repository
Tester des scénarios existants
Module 1
Vous avez créé un InMemoryRepository — et c'était un OUTIL DE TEST !
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
🐌
Tester avec PostgreSQL
⚡
Tester avec InMemory
🎯 On teste la logique métier, pas la base de données
Module 2
PostgreSQL en prod · InMemory en test
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 !
🏭 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
Vous le faites depuis S11 !
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
// 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
Créer, vérifier, erreurs
Créer un InMemoryRepository vide
Dans beforeEach — un repo frais pour chaque test
Injecter le repo dans le use case
new EnrollStudent(inMemoryRepo)
Appeler le use case et vérifier le résultat
expect(result) / expect(repo.findById) / toThrow()
beforeEach(() => {
repo = new InMemoryStudentRepo();
useCase = new EnrollStudent(repo);
});
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
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 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
Un use case n'a pas qu'un seul chemin — il faut tester chaque branche
✅ Chemin succès
❌ Chemins erreur
// 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
Tester des scénarios avec données existantes
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
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
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([]);
});
❌ 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 !
});
❌ 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.
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
🧪
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
L'InMemoryRepository : le pont entre S10 et aujourd'hui
Prochaine étape : pratiquer l'injection de dépendances sur vos propres use cases