Un premier invariant
Quelles sont les règles métier à respecter pour la création d'une réservation ?
Certaines sont évidentes :
- le logement demandé ne doit pas être déjà loué
- la date d'arrivée doit être dans le futur
- la date de départ doit être après la date d'arrivée
- la taille du logement est limitée. Il ne peut pas accueillir plus de personnes que le max prévu.
Quoi d'autre ?
Discutons avec l'expert métier, et posons-lui des questions.
- Un vacancier qui a déjà une réservation pour un logement peut-il en faire une autre sur une même période ?
- Un vacancier qui a un litige en cours peut-il faire une réservation ?
Toutes ces règles à respecter sont des invariants métier.
Découper et ordonner
Quelle est l'activité favorite des développeurs à l'école maternelle ?
Le découpage.
C'est une compétence essentielle (et sous-estimée) du métier.
En découpant finement les fonctionnalités, le code évolue de façon continue. Par petits incréments très rapprochés.
Les invariants sont des fonctionnalités importantes. Ils garantissent l'intégrité des données. Un système de réservation permettant de faire n'importe quoi ne sert à rien.
Alors commençons par un invariant, et un seul pour découper finement. Par exemple :
- la date d'arrivée d'une nouvelle réservation doit être dans le futur
Et comment procède-t-on ? Avec un test bien sûr !
Un nouveau test
Écrivons la tentative de réservation dans le passé.
const app = new App(testDependencies())
// Tentative de réservation dans le passé
const session = await app.run([
login({email:"faketenant@mail.com", password:"secret"}),
book({
accommodationId:"accommodation-1",
adults:2, children:3,
from : new Date("2023-06-02"),
to : new Date("2023-06-02"),
})
]);
expect(session.error).toEqual(InvalidInterval())Et là, un problème se pose. Il faut choisir une date passée. Est-ce que le 2 juin 2023 est une date passée ?
Tout dépend de la date du jour au moment de l'exécution du test !
Alors une façon consiste à choisir une date très loin dans le passé. Genre le 2 juin 1854. Mais on voit combien ce choix est fragile. D'ailleurs, notre test précédent utilisait la date du 02 juin 2024.
Le 1er juin 2024, le test passe. Et soudain, le 3 juin, il ne passe plus !
La solution : considérer la fonction qui retourne la date courante comme une dépendance.
Le service dateProvider
L'horloge interne, tout comme l'accès à la base de données, est une dépendance.
Revisitons donc les dépendances injectées dans notre classe App, en leur ajoutant un service chargé de fournir la date courante :
export const testDependencies = () => ({
users: new MemoryUserRepository(),
bookings: new MemoryBookingRepository(),
dateProvider: /* ... c'est ce qu'il nous reste à écrire */
})Lorsqu'il s'agira de mettre en service notre application, on utilisera un provider retournant la date système :
export const systemDateProvider = {
now : () => Date.now()
}Abstraction de la représentation d'une date
Mais pourquoi la date du jour serait-elle générée par Date.now(), qui retourne le nombre de millisecondes écoulées depuis le 01/01/1970 ?
Et non pas new Date() qui retourne un objet Date standard de JavaScript ?
Comment souhaite-t-on représenter une date dans notre application ? C'est une question cruciale qui risque de revenir tout casser lorsqu'il s'agira de comparer les dates de fuseaux horaires différents !
Imaginons qu'un utilisateur au Mexique cherche à réserver un logement de vacances en France. Quelle serait la façon naturelle de gérer les dates de sa réservation ?
Discutons-en avec l'expert métier. Il nous fournit une indication précieuse :
les dates (et heures) de réservation sont gérées sur le fuseau horaire du lieu du logement.
Si le logement est en France, alors on affiche ses dates de réservation ou de disponibilité selon le fuseau Europe/Paris.
Vous aurez l'impression de réinventer la roue. Mais vous avez tout intérêt à construire votre propre abstraction des dates gérées dans votre application.
Faisons au plus simple, nous pourrons toujours améliorer cela plus tard.
export const systemDateProvider = {
now: () => Date.now(),
toDate: (something) => new Date(something).valueOf(),
sameDay: (date1, date2, timeZone = "Europe/Paris") =>
new Date(date1).toLocaleDateString("fr-FR", { timeZone }) ===
new Date(date2).toLocaleDateString("fr-FR", { timeZone }),
isBefore: (date1, date2) => date1 < date2,
}Deux pièges à éviter dans ces quelques lignes.
La méthode s'appelle toLocaleDateString(), pas toLocaleDate() — cette dernière n'existe pas en JavaScript.
Et son premier argument est une locale (fr-FR), pas un fuseau : le fuseau se passe dans les options, et il s'écrit avec un identifiant IANA (Europe/Paris), jamais avec un code pays.
Injection d'un provider de test
Mais pour nos tests, nous utiliserons un autre provider que ce systemDateProvider.
Pour tester les fonctionnalités liées à la date courante, nous avons besoin d'un provider qui retourne une date arbitraire et fixe.
Pour notre test, imaginons que la date du jour soit le 12 juin 2023.
Une réservation sur le 02 juin 2023 est alors dans le passé de façon certaine. Peu importe la date à laquelle le test est exécuté.
const testDateProvider = {
...systemDateProvider,
now: () => new Date('2023-06-12').valueOf()
}Notez le .valueOf(). Le now() du provider système retourne un nombre de millisecondes ; celui du provider de test doit retourner la même chose.
Deux implémentations d'une même dépendance qui ne retournent pas le même type, c'est un bug qui attend son heure.
Ajoutons ce provider à nos dépendances de test.
export const testDependencies = () => ({
users: new MemoryUserRepository(),
bookings: new MemoryBookingRepository(),
dateProvider: testDateProvider,
})Et voilà. C'est comme si tous nos tests s'exécutaient indéfiniment le 12 juin 2023.
Ce problème de date du jour étant réglé, laissons le test nous guider pour affiner notre commande de réservation.