Le premier parcours utilisateur
Nous avons eu chaud, mais le parcours utilisateur de réservation d'un utilisateur déjà inscrit sur la plateforme est bien celui-là :
- se connecter à la plateforme
- disposer d'un bandeau comportant une date de début / fin de réservation
- voir tous les logements dont la location est disponible à cette date
- choisir un logement et le réserver en 1 clic
- voir sa réservation dans une page "mes réservations"
Avant d'écrire tout le HTML/CSS nécessaire pour faire ça, arrêtons-nous une minute.
Pour d'abord écrire un test.
Test du parcours utilisateur
En effet, ce parcours ne correspond pas exactement à notre test :
const app = new App(testDependencies());
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-04"),
}),
]);
const bookings = await app.dependencies.bookings.listBookingsForAccomodationId("accomodation-1");
expect(bookings).toHaveLength(1);
Il manque 2 étapes :
- voir tous les logements dont la location est disponible à cette date
- voir sa réservation dans une page "mes réservations"
Pourquoi ?
Parce que ce ne sont pas des commandes qui changent l'état du système.
Ce sont des requêtes qui permettent de lire l'état du système.
Dans notre test, nous avons eu besoin de lire l'état du système tout à la fin. Pour vérifier que la commande avait bien été ajoutée à la liste des réservations.
const bookings = await app.dependencies.bookings.listBookingsForAccomodationId("accomodation-1");
expect(bookings).toHaveLength(1);
Alors ajoutons les 2 requêtes manquantes.
Liste des réservations d'un vacancier
La plus simple est celle consistant à obtenir la liste des réservations d'un vacancier dans sa page "mes réservations".
Elle ressemble beaucoup à la requête déjà utilisée :
// Liste des réservations sur un logement
const bookings = await app.dependencies.bookings.listBookingsForAccomodationId("accomodation-1");
expect(bookings).toHaveLength(1);
// Liste des réservations détenues par un vacancier
const bookingsOfTenant = await app.dependencies.bookings.listBookingsForTenantId("tenant-1");
expect(bookingsOfTenant).toHaveLength(1);
// Encore mieux : les 2 réservations doivent être identiques
expect(bookingsOfTenant[0]).toEqual(bookings[0]);
Lançons notre test, qui indique comme attendu que la fonction listBookingsForTenantId
n'existe pas.
Alors ajoutons-la simplement à notre dépendance :
export class MemoryBookingRepository {
_bookings = [];
async save(booking) {
this._bookings.push(booking);
}
async listBookingsForAccomodationId(accomodationId) {
return this._bookings.filter(
(booking) => booking.accomodationId === accomodationId
);
}
async listBookingsForTenantId(tenantId) {
return this._bookings.filter(
(booking) => booking.tenantId === tenantId
);
}
}
Et ... le test ne passe pas !
Il échoue sur l'assertion
expect(bookingsOfTenant[0]).toEqual(bookings[0]);
En creusant la cause de cet échec, on s'aperçoit d'un bug dans notre commande book()
.
Lors de la création de la réservation, nous avons conservé en l'état les paramètres de début et fin, sans utiliser notre abstraction des dates !
const booking = {
tenantId: user.id,
accomodationId,
hosts: { adults, children },
// Ancien code : interval: { from, to },
// Que l'on remplace par :
interval: { from: toDate(from), to: toDate(to) },
};
Un joli bug détecté au passage !
Poursuivons le travail pour l'autre requête qui est plus complexe.
Liste des logements disponibles
L'objectif est d'obtenir la liste de tous logements dont la location est possible sur une période.
Le piège serait de basculer directement dans l'écriture d'une requête SQL complexe. Et d'ailleurs, à qui enverrait-on cette requête ? Nous n'avons pas encore évoqué la base de données !
Retournons la question. En l'absence de base de données, comment devrait-on faire ?
Nous avons besoins de croiser 2 sources de données différentes :
- la liste des réservations validées
- la liste des logements.
Voici une façon de faire :
- recenser toutes les réservations concernées par une période [début, fin]
- lister tous les logements qui ne sont pas concernés par ces réservations
Avant de coder cette nouvelle fonction dans notre MemoryBookingRepository
, écrivons un test.
const app = new App(testDependencies());
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-04"),
}),
]);
const bookings = await app.dependencies.bookings.listBookingsForAccomodationId("accomodation-1");
expect(bookings).toHaveLength(1);
const bookingsOfTenant = await app.dependencies.bookings.listBookingsForTenantId("tenant-1");
expect(bookingsOfTenant).toEqual(bookings[0]); // on parle de la même réservation que précédemment
// Requête sur une période comportant déjà une réservation
const unavailableAccomodations = await app.dependencies.bookings.getAvailableAccomodations({
from: toDate("2024-06-01"),
to: toDate("2024-06-03")
});
expect(unavailableAccomodations.some(accomodation => accomodation.id ==="accomodation-1")).toBe(false);
// Requête sur une période sans réservation
const availableAccomodations = await app.dependencies.bookings.getAvailableAccomodations({
from: toDate("2024-05-12"),
to: toDate("2024-05-15")
});
expect(availableAccomodations.some(accomodation => accomodation.id ==="accomodation-1")).toBe(true);
Comme attendu, le test échoue sur l'absence de la fonction getAvailableAccomodations
.
Ajoutons-la à notre dépendance.
Astuce : commencez par écrire le gabarit de la fonction, en retournant une liste vide :
async getAvailableAccomodations({ from, to }) {
return [];
}
Relancez votre test, et observez l'erreur obtenue. Il ne s'agit plus désormais de l'absence de la fonction, mais de l'absence de logement.
AssertionError: expected false to be true // Object.is equality
- Expected
+ Received
- true
+ false
src/booking/domain/tests/book.test.js:116:5
114| (accomodation) => accomodation.id === "accomodation-1"
115| )
116| ).toBe(true);
| ^
117| });
118|
Complétons cela pour faire passer le test au vert.
async getAvailableAccomodations({ from, to }) {
const bookedAccomodationsIds = this._bookings
.filter((booking) => isOverlapped(booking.interval, { from: toDate(from), to: toDate(to) }))
.map((booking) => booking.accomodationId);
return this._accomodations.filter(
(accomodation) =>
!bookedAccomodationsIds.some((id) => id === accomodation.id)
);
}
Au passage, nous avons créé une fonction isOverlapped
nécessaire pour déterminer les réservations qui recouvrent l'intervalle indiqué.
Et tous les tests sont au vert.
De l'intérêt de ré-écrire une base de données
Quel intérêt de passer du temps sur ce MemoryBookingRepository
?
Il ne sert qu'aux tests.
Dans la vraie version en production, il sera remplacé par un SQLBookingRepository
.
Alors pourquoi s'évertuer à le coder juste pour le plaisir de faire passer un test au vert ?
C'est en effet discutable. Mais cette approche possède 2 bénéfices intéressants :
-
Elle permet d'avoir une app fonctionnelle, même sans base de données installée. Cela accélère la possibilité de faire une démo rapidement. Et même d'onboarder un développeur ou un designer sans besoin d'installer grand chose sur son poste de dev.
-
Elle contraint le service d'accès aux données Elle oblige à définir des API simples, en nombre réduits. On bénéficiera d'un effet de sobriété vertueux sur le long terme.
-
Elle va guider le choix futur de la base de données. En concevant une dépendance de test, on plonge plus précisémment sur les fonctionnalités attendues de notre futur base de données. Les choix techniques peuvent donc être repoussés, afin qu'ils soient guidés par les fonctionnalités réellement attendues pour notre projet. On évite dans le piège d'une décision trop anticpée. "Ah mince, on n'aurait jamais du choisir MongoDb".
Maintenant, nous avons tout le nécessaire pour nous lancer dans l'UI.