Première version de notre application
La première version doit faire passer ce test.
it("A tenant can book an accomodation", async () => {
// Injection des dépendances
const app = new App(testDependencies());
// Réalisation de la location
await app.run([
login({ email: "faketenant@mail.com", password: "secret" }),
book({
accomodationId: "accomodation-1",
adults: 2,
children: 3,
from: new Date("2024-06-02"),
to: new Date("2024-06-02"),
}),
]);
// Le calendrier comporte la réservation
const bookings =
await app.dependencies.bookings.listBookingsForAccomodationId(
"accomodation-1"
);
expect(bookings).toHaveLength(1);
});
Installons un outil de test pour réaliser cela. J'ai une préférence pour vitest, mais n'importe quel outil peut faire l'affaire.
Créons notre projet et installons vitest
mkdir projet-booking
cd projet-booking
yarn init
yarn add -D vitest
Ajoutons un script dans le fichier package.json
pour lancer l'exécution des tests :
{
"name": "mon-projet-booking",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"test": "vitest"
}
}
Reprenons ce que nous avons écrit jusqu'à présent pour y parvenir.
La commande de login
function login(payload) {
const {email, password} = payload;
return async function (dependencies, context) {
const user = await dependencies.users.findByEmail(email);
if (!user) {
// Utilisateur inconnu
return context.withError(UnknownUserEmail(email))
}
if (user.encryptedPassword !== encrypt(password)) {
// Erreur de mot de passe
return context.withError(WrongPassword(user))
}
// Tout est OK, ajoutons l'utilisateur au contexte
// pour les commandes suivantes qui en auront besoin
return context.withUser(user)
}
}
function UnknownUserEmail(email) {
return new Error(`Unknown user ${email}`)
}
function WrongPassword(user) {
return new Error(`Wrong password ${user.email}`)
}
export function encrypt(text) {
return text; // TODO : utiliser une fonction pour chiffrer le texte
}
La commande de réservation
function book(payload) {
const {accomodationId,adults,children,from,to} = payload;
return async function (dependencies, context) {
const user = context.loggedUser; // Peut-être null !
if (!user) {
return context.withError(shouldBeLogged())
}
const booking = {
tenantId:user.id,
accomodationId,
hosts : {adults,children},
interval : {from,to}
};
await dependencies.bookings.save(booking);
return context
}
}
function shouldBeLogged() {
return new Error("User should be logged in")
}
Les dépendances de test
class MemoryUserRepository {
// On indique par défaut un utilisateur déjà enregistré
_users = [
{id:"tenant-1", email:"faketenant@mail.com", encryptedPassword:encrypt("secret")}
]
async findByEmail(email) { return this._users.find(user => user.email === email)}
}
class MemoryBookingRepository {
_bookings = [];
async save(booking) { this._bookings.push(booking) };
async listBookingsForAccomodationId(accomodationId) {
return this._bookings.filter(booking => booking.accomodationId === accomodationId)
}
}
export const testDependencies = () => ({
users : new MemoryUserRepository(),
bookings : new MemoryBookingRepository()
})
La classe de contexte
class Context {
withError (error) {
this.error = error;
return this;
}
withUser (user) {
this.loggedUser = user;
return this;
}
isOk() {
return !this.error
}
}
La classe App
class App {
constructor (dependencies) {
this.dependencies = dependencies;
}
async run(usecases) {
var context = new Context()
for (const usecase of usecases) {
// Inutile de continuer en cas d'erreur
if (context.isOk()) {
context = await usecase(this.dependencies, context)
}
}
return context
}
}
Organisation des répertoires
Organisons les différents fichiers dans notre projet.
On peut séparer les dépendances dans un répertoire infra
afin de ne pas les mélanger avec le coeur de notre application que l'on regroupe dans un répertoire domain
.
Comme nous n'avons que 2 commandes pour l'instant, adoptons une organisation simple avec 3 sous répertoires de domain
:
app
: contient les classes utilitairesApp
etContext
tests
: pour y mettre notre fichier de test (mais d'autres suivront bientôt)usecases
: pour y mettre les commandes
projet-booking
|- domain
|- app
|- App.js
|- Context.js
|- tests
|- book.test.js
|- usecases
|- book.js
|- login.js
|- infra
|- MemoryBookingRepository.js
|- MemoryUserRepository.js
|- testDependencies.js
Le code complet de cette première version est disponible ici (opens in a new tab).
Résultat du test
Et là, sous nos yeux, le test passe au vert.
✓ src/booking/domain/tests/book.test.js
✓ A tenant can book an accomodation
Test Files 1 passed (1)
Tests 1 passed (1)
Et ce n'est que le début !