Controllers + Tests

Endpoints REST, tests unitaires et intégration complète

Bootcode IWA-S04 — Semaine 21, Jour 4

Objectifs du jour

1. Créer au moins 3 endpoints REST fonctionnels

GET, POST, et un endpoint d'action métier

2. Écrire au moins 4 tests unitaires qui passent

Avec repositories in-memory — pas de base de données

3. Faire fonctionner le flux complet

HTTP → Controller → Use Case → Repository

4. Préparer une démo fonctionnelle pour demain

Un scénario qui montre le flux de bout en bout

Plan du cours

1

Rappel : le controller reçoit les use cases par injection

Et délègue — pas de logique métier dans le controller

2

Tips : l'ordre d'implémentation

Tests des use cases → controller → intégration

3

Format de présentation de demain

10 min par étudiant : 5 min démo + 5 min questions

4

Dernier sprint

Aider les étudiants à finir et préparer leur démo

Le rôle du controller

Recevoir HTTP, déléguer aux use cases, retourner une réponse

« Le controller est la dernière couche. Il ne contient PAS de logique métier. »

Le flux complet

Du HTTP au repository — chaque couche délègue à la suivante

HTTP Request Controller Use Case Agrégat Repository

DB / InMemory Agrégat Output JSON Response HTTP Response

Controller

Parse le body, appelle le use case, formate la réponse

Use Case

Orchestre : charge, appelle la méthode métier, sauvegarde

Agrégat

Valide l'invariant, produit l'événement

Repository

Persiste / charge via mapper + entity

Un controller Express

Injecte les use cases via le container, délègue, formate la réponse

@injectable()

export class BookController {

constructor(

@inject('AddBook') private addBook: AddBook,

@inject('BorrowBook') private borrowBook: BorrowBook

) {}

borrow = async (req, res) => {

try {

const output = await this.borrowBook.execute(req.body);

res.status(200).json(output);

} catch (e) {

if (e instanceof DomainError) res.status(400).json({ error: e.message });

else res.status(500).json({ error: 'Internal error' });

}

}

}

Controller avec logique vs sans logique

❌ Logique métier dans le controller

borrow = async (req, res) => {

const book = await repo.findById(req.body.bookId);

if (book.isBorrowed) return res.status(400);

book.isBorrowed = true; // ❌ mutation directe

await repo.save(book);

}

L'invariant n'est pas protégé. Duplication de la logique. Impossible à tester.

✅ Controller qui délègue

borrow = async (req, res) => {

try {

const out = await this.borrowBook.execute(req.body);

res.json(out);

} catch (e) { /* ... */ }

}

Le use case + l'agrégat gèrent la logique. Le controller est mince et testable.

Tester un use case

Avec un InMemoryRepository — pas de base de données, pas de mock complexe

describe('BorrowBook', () => {

it('emprunte un livre disponible', async () => {

const repo = new InMemoryBookRepository();

const book = Book.init(BookId.create(), ISBN.create('978...'));

await repo.save(book);

const usecase = new BorrowBook(repo);

const result = await usecase.execute({ bookId: book.id.value, memberId: 'm1' });

expect(result.loanId).toBeDefined();

expect(result.dueDate).toBeInstanceOf(Date);

});

}

Tester les cas d'erreur

Chaque DomainError doit avoir un test qui prouve qu'elle se déclenche

it('refuse d'emprunter un livre déjà emprunté', async () => {

const repo = new InMemoryBookRepository();

const book = Book.init(...);

book.borrow(MemberId.create('m1')); // déjà emprunté

await repo.save(book);

const usecase = new BorrowBook(repo);

await expect(usecase.execute({ bookId: book.id.value, memberId: 'm2' }))

.rejects.toThrow(BookAlreadyBorrowedError);

});

💡 Objectif : au moins 4 tests. Un test par use case + un test par cas d'erreur important.

Câbler les routes Express

Le controller est résolu depuis le container, puis branché sur Express

const container = new Container();

LibraryBuild.build(container); // bind repositories + use cases

container.bind('BookController').to(BookController);

const app = express();

app.use(express.json());

const controller = container.get('BookController');

// Routes

app.post('/books', controller.add);

app.post('/books/:id/borrow', controller.borrow);

app.post('/books/:id/return', controller.return);

app.get('/loans/overdue', controller.listOverdue);

Préparer la démo de demain

Un scénario qui montre le flux de bout en bout — simple et qui marche

1️⃣

Scénario en 4-5 étapes

Ajouter un livre → emprunter → tenter un double emprunt (erreur) → rendre → lister les retards

2️⃣

Tester avec curl ou Postman

Préparez les commandes à l'avance — pas d'improvisation live

3️⃣

Montrer l'erreur métier

Le double emprunt doit renvoyer un 400 avec un message clair — ça prouve que l'invariant fonctionne

4️⃣

Mieux vaut simple qui marche

Un projet modeste qui démontre le flux > un projet ambitieux qui plante en démo

Pièges courants

❌ Passer trop de temps sur le controller

Le controller est la couche la plus simple. Codez d'abord les tests, le controller vient après.

⚠️ Oublier de préparer la présentation

La démo de demain compte autant que le code. Préparez votre scénario et vos commandes curl.

❌ Paniquer si tout n'est pas fini

La démo peut montrer ce qui fonctionne. Mieux vaut 3 use cases qui marchent que 10 qui plantent.

⚠️ Mettre de la logique métier dans le controller

Le controller délègue. Si vous avez un if (book.isBorrowed) dans le controller, c'est une erreur.

À retenir !

✅ Les tests unitaires utilisent des repositories in-memory — pas de base de données.

✅ Le controller est la dernière couche — il ne contient PAS de logique métier.

✅ Préparez la démo : un scénario qui montre le flux de bout en bout.

✅ Mieux vaut un projet simple qui fonctionne qu'un projet ambitieux qui plante.

Demain : présentations & démos — célébration ! 🎉