📐 Construire une app en JS
18 Ameliorer Reservation

Améliorer notre commande

Que faut-il au minimum pour satisfaire le test d'un réservation dans le passé ?

D'abord exécutons-le pour nous assurer qu'il échoue. Vous serez surpris du nomvre de fois ou un test est vert sans avoir rien fait (souvent, c'est mauvais signe!).

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({
        accomodationId:"accomodation-1", 
        adults:2, children:3, 
        from : new Date("2023-06-02"), // Sûr d'être dans le passé
        to :   new Date("2023-06-02"),
    })
]);
 
expect(session.error).toEqual(InvalidInterval())

Ouf, nous avons bien un échec :

FAIL  src/booking/domain/tests/book.test.js > A tenant cannot book an accomodation int he past
AssertionError: expected undefined to be Error: This date interval is not correct // Object.is equality
 
- Expected: 
[Error: This date interval is not correct]
 
+ Received: 
undefined

Corrigeons la commande pour régler ça.

Ajout de l'invariant dans notre commande

Rien de compliqué. Il suffit de vérifier que la date de début est dans le futur. Peu importe la date de fin.

function book(payload) {
    const {accomodationId,adults,children,from,to} = payload;
    return async function (dependencies, context) {
        const user = context.loggedUser;
        if (!user) {
            return context.withError(shouldBeLogged())
        }
        const {toDate, isBefore, now} = dependencies.dateProvider
        if (isBefore(toDate(from), now())) {
            return context.withError(InvalidInterval())
        }
        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")
}
 
function InvalidInterval() {
    return new Error("This ")
}

Bingo, ces 3 lignes suffisent à passer le test.

Aller à un rythme tranquille

Lorsqu'on modifie notre commande book, un point saute aux yeux : il nous faut valider que la date de fin de réservation est après la date de début. C'est un cas d'erreur évident.

La tentation est grande d'aller vite en ajoutant le bout de code qui test aussi cela.

Mais adoptons une attitude différente : s'interdire de toucher au code de la commande tant que tous les tests passent.

Donc gardons notre flow et ajoutons un test. Sur un malentendu, il pourrait déjà être au vert.

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({
        accomodationId:"accomodation-1", 
        adults:2, children:3, 
        from : new Date("2023-06-22"), // Dans le futur (notre provider de test est figé au 12 juin 2023)
        to :   new Date("2023-06-12"), // Oups..
    })
]);
 
expect(session.error).toEqual(InvalidInterval())

Comme attendu le test échoue, nous donnant le droit de modifier le code.

Attardons nous sur la condition à vérifier : faut-il que le jour to soit strictement supérieur à from ? Ou les dates peuvent-elle être égales ?

Posons la question à l'expert métier. Il vous répond que l'on réserve un nombre de nuitées. Donc que la date de départ doit être a minima le lendemain de la date d'arrivée. Une égalité est un cas d'erreur.

Cela interroge la fonction isBefore de notre provider de date. Nous voulons qu'elle retourne vrai lorsque les dates sont égales.

Cela appelle 2 questions :

  1. Ne devrait-on pas renommer la fonction isBefore par isBeforeOrEqual pour ne plus se poser la question plus tard ?

Oui probablement.

  1. Est-ce que les endroits ou cette fonction est déjà utilisé s'attendent bien à ce comportement ?

Bonne question. Nous avons déjà utilisé cette fonction pour vérifier que la réservation commmence dans le futur. Mais peut-on faire une réservation qui commence le jour même ?

Retournons voir l'expert métier, qui nous dit qu'il y a un préavis d'au moins 1 jour. Et voilà un nouvel invariant qui mérite lui aussi sont test !

Test du cas d'égalité

Le test d'un réservation sans respecter la minimum d'une nuitée

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({
        accomodationId:"accomodation-1", 
        adults:2, children:3, 
        from : new Date("2023-06-22"), 
        to :   new Date("2023-06-22"), // Non respect d'une nuitée mini
    })
]);
 
expect(session.error).toEqual(InvalidInterval())

Nous avons désormais deux tests qui échouent. Et qui nous guident sur les modifications à réaliser.

Modifications du provider de date et de la commande de réservation

D'abord, attachons nous à clarifier le provider de date pour transformer la fonction isBefore en isBeforeOrEqual :

export const systemDateProvider = {
  now: () => Date.now(),
  toDate: (something) => new Date(something).valueOf(),
  sameDay: (date1, date2, timezone = "FR-fr") =>
    new Date(date1).toLocaleDate(timezone) ===
    new Date(date2).toLocaleDate(timezone),
  isBeforeOrEqual: (date1, date2) => date1 <= date2,
};

Puis apportons les modifications à notre commande :

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 { toDate, isBeforeOrEqual, now } = dependencies.dateProvider;
    if (isBeforeOrEqual(toDate(from), now())) {
      return context.withError(InvalidInterval());
    }
    if (isBeforeOrEqual(toDate(to), toDate(from))) {
      return context.withError(InvalidInterval());
    }

Relançons les tests. Et surprise :

❯ src/booking/domain/tests/book.test.js (3)
   × A tenant can book an accomodation
A tenant cannot book an accomodation int he past
A tenant should provide a valid interval

Les 2 noouveux tests passent. Mais le premier ne passe plus !

Pourquoi ?

Parce notre premier test, écrit tout au début, invoquait cette commande :

book({
    accomodationId:"accomodation-1", 
    adults:2, children:3, 
    from : new Date("2024-06-02"), // C'est bien dans le futur, notre provider de test est figé en 2023 !
    to :   new Date("2024-06-02"),
})

Cela nous avait échappé ! En ajoutant l'invariant sur le nombre de nuitées minimum, ce test échoue désormais.

Corrigeons-le pour le mettre en cohérence avec le métier, en remplaçant la date de fin par "2024-06-04". Ainsi, tous nos tests passent.

Le flow de réalisation du code

Voilà notre backend sur les rails.

En conclusion, revenons sur notre démarche de développement qui s'articule en itérations comportant 3 étapes :

  1. Réfléchir à ce que le code doit faire, en impliquant l'expert métier
  2. Code un test qui contrôle l'attendu, et s'assurer qu'il échoue
  3. Apporter les modifications minimales pour faire passer le test au vert.

Cette démarche apporte beaucoup de sérénité. Ce flow est incroyablement efficace. Vous serez surpris de la productivité obtenue. Elle s'explique par des itérations qui forcent à découper un problème complexe en problèmes plus petits, et par des tests qui garantissent qu'il n'y a pas de retour en arrière.

On gravit la pente pas-à-pas, sans fatigue, à rythme constant, et sans jamais chuter en arrière !

C'est à ce moment que votre client vous appelle.