Aller au contenu

Mocks et services workers : une combinaison gagnante

Introduits pour la première fois dans les années 2000, les mocks continuent pourtant d'évoluer dans leurs usages. En les combinant avec une technologie plus récente, les service workers, il devient possible d'intercepter les requêtes réseau afin d'imiter le comportement de n'importe quelle API REST.

La Fausse Tortue ou Simili Tortue dans Les Aventures d'Alice au pays des merveilles

Je vous l'accorde, c'est un titre ambitieux, d’aucuns diront même racoleur. Et pourtant, nous allons le voir tout au long de cet article, l'utilisation astucieuse des mocks et des services workers peut permettre de décupler la production de valeur des équipes de développement.

Si vous avez déjà été confronté à des problèmes de connectivité (Network, VPN...), de développement en amont de phase, d'instabilité des environnements de développement, de scénarios alambiqués... vous avez déjà une petite idée des irritants qui peuvent vous ralentir. Ce sont des problèmes récurrents dans le monde du développement web. Avec l'aide de la librairie msw, j'entends vous montrer comment faire en sorte qu'ils deviennent des problématiques du passé.

Mise en situation

Voici quelques péripéties, un peu romancées, auxquelles j'ai déjà été confronté. Vous vous reconnaitrez peut-être dans certaines d'entre elles.

Mercredi matin, 10h, au début du sprint, je m'assigne la tâche la plus prioritaire du board. Cette dernière consiste à consommer une nouvelle API Rest. Cependant, cette API n'est pas encore disponible, seule la signature existe. Je souhaiterais néanmoins entamer le développement sans devoir me soucier de cette entrave, en consommant cette API comme si elle était d'ores et déjà opérationnelle.
Un plan d'architecte d'un étage d'une maison
Jeudi, milieu de journée. Je suis en train de réaliser l'affichage d'une liste de produits alimentée par une API Rest quand soudain, sans prévenir, je ne suis plus en mesure de joindre cet API (perte de réseau, problème de VPN, instabilité de l'env de dev...). Je souhaiterais pourtant pouvoir continuer à avancer même quand je ne suis pas en mesure de contacter une ressource.
Une prise électrique débranchée
Mardi, 9h. Call avec le métier, nous avons un bug sur la nouvelle release. Le scénario permettant de réitérer ce dernier est alambiqué. Il est très difficile de trouver un jeu de données permettant de le reproduire sur l'environnement de développement. Pourtant, je désirerais pouvoir m'atteler au correctif sans avoir à passer des heures à chercher des données.
Une aiguille dans une botte de foin
Lundi, début d'après-midi. Réunion de chapter dev. Nous évoquons la mise en œuvre de la résilience au sein de nos applicatifs. Plus spécifiquement, le comportement des SPAs lorsqu'un call API est en erreur. Nous souhaiterions pouvoir reproduire facilement ces cas d'usages afin de tester ces comportements sans souffrance.
Du café renversé sur des documents

Si vous aussi, vous vous êtes déjà retrouvés dans une de ces situations, alors cet article est fait pour vous. Pour les autres, j'espère quand même que vous y trouverez du contenu qui pourra vous être utile.

Adoption par les pionniers

La bibliothèque msw (aka Mock Service Worker) ne m'a été révélée que très récemment (fin 2022). J'étais dans un premier temps sceptique face à l'utilisation d'un service worker pour la mise en œuvre de mocks d'API Rest. Cela me semblait très complexe, voire trop complexe, pour une valeur ajoutée que je trouvais finalement assez faible. Jusqu'à présent, j'utilisais plutôt le pattern IoC (avec InversifyJS) pour faire l'injection de services mockés afin d'obtenir le résultat escompté. Cependant, la curiosité l'ayant emporté, j'ai franchi le cap pour voir ce que cette librairie pouvait apporter de plus à mon projet. J'ai très rapidement compris qu'elle changeait la donne. L'API est très simple à comprendre, et la mise en œuvre est d'une facilité déconcertante. D'ailleurs, à voir la courbe de téléchargement, je ne suis pas le seul à l'avoir adopté. msw ne fait que gagner en popularité depuis la mi-2020, et la tendance ne semble pas ralentir.

La fréquence de téléchargement de msw

Mise en œuvre

Voyons ensemble les différentes modifications nécessaires à la mise en œuvre de cette nouvelle bibliothèque. Je ne vais pas trop rentrer dans les détails puisque la documentation disponible sur le site de msw est de très bonne facture et le fera bien mieux que moi.

Dans un premier temps, au lancement de l'application, il est nécessaire de démarrer le worker msw. Ce code doit être conditionnel afin de n'activer ce mode de fonctionnement dégradé qu'à la demande. Dans l'exemple suivant, j'ai fait le choix de conditionner ce démarrage avec des query param mais vous êtes libre de le faire autrement. Au sein de cette condition, on retrouve un import dynamique des ressources afin de ne pas alourdir l'application dans le mode de fonctionnement nominal (sans les mocks).

const searchParams = new URLSearchParams(location.search);

if (searchParams.get('mock') === 'true') {
    const { worker } = await import('mocks/browser');
    worker.start();
}
D'où vient le script mocks/browser ?

C'est un fichier à ajouter dans la base de code. J'ai fait le choix de le placer dans un dossier mocks mais ce n'est pas une obligation. On y retrouve la construction du worker qui est alimenté avec une liste de handlers.

import { setupWorker } from 'msw';
import { handlers } from './handlers';

export const worker = setupWorker(...handlers);
Super, et je fais comment pour définir ces handlers ?

Les handlers sont définis dans un autre fichier afin de pouvoir être aussi utilisés dans les tests.

import { RestHandler } from 'msw';
import {
	getProductsWithEmptyDataMock,
} from 'services/products/products.mock';

export const handlers: ReadonlyArray<RestHandler> = [
    getProductsWithEmptyDataMock(),
    // ...
];
Euh, c'est quoi ce getProductsWithEmptyDataMock() ?

C'est une fonction de mock qui va nous permettre de décrire le comportement du service qui va être restitué par le service worker. Dans l'exemple suivant, le mock va définir le comportement attendu lors d'une requête get sur l'url url/de/mon/api. La réponse est un code 200 avec un délais de réponse de 150 ms et qui va restituer un tableau vide.

import { rest } from 'msw';

export const getProductsWithEmptyDataMock = () => rest.get(
    'url/de/mon/api',
    async (req, res, ctx) => {
        return res(
            ctx.status(200),
            ctx.delay(150),
            ctx.json([]);
        );
    },
);

Ce dernier exemple de code est à reproduire pour chaque service consommé par votre application. Sans définition spécifique, le service worker se comportera en passe-plat de l'API Rest.

Quelques conseils

Avant d'aller plus loin sur ce qu'il est possible de faire avec msw, je souhaite vous faire part de quelques conseils que j'ai rassemblé tout au long de l'utilisation de cette librairie.

Keep it simple, stupid

Utilisez des mocks simples, les plus simples possible. En effet, une complexification inutile des mocks compliquera leur maintenance et leur évolution, en plus de nécessiter une durée significative à leur réalisation. Il est préférable de multiplier les mocks pour une même API Rest plutôt que de recourir à une logique qui s'avère difficile à appréhender. De cette manière, ajouter un nouveau cas d'utilisation devient un exercice simple ; il suffit de créer un nouveau mock.

Délais aléatoires

Utilisez des délais de réponse aléatoires afin de mieux appréhender les latences possibles au sein de l'application. De plus, si votre application comporte des conditions de concurrence, vous y serez confrontés avant d'arriver en production. La façon la plus simple de le faire est d'utiliser le ctx.delay() (sans paramètre), et c'est msw qui se chargera d'utiliser une latence réaliste pour votre appel API. Vous pouvez aussi construire vous-même une fonction de génération de délais afin d'être plus pessimiste sur les latences.

Gestion des erreurs

Profitez de cet outil pour tester le comportement de votre application en cas d'erreur de vos API Rest. Ainsi, au lieu d'avoir une page qui freeze, vous serez en mesure de détecter les redirections manquantes vers des pages d'erreur ou l'affichage de bandeaux indiquant l'indisponibilité du service.

Nouveaux cas d'usage

Continuez à enrichir la base de mock avec les nouveaux cas d'usage que vous découvrirez au fur et à mesure. Que ce soit un bug détecté par les QAs, un cas de test inhabituel... L'objectif est d'avoir suffisamment de variété pour soumettre l'application à du stress et ainsi découvrir au plus tôt les anomalies critiques pour les corriger rapidement.

Aller plus loin avec les scénarios

Le potentiel de msw prend réellement sa pleine mesure avec l'introduction des scénarios. Ces derniers ne sont pas une construction propre à msw mais plutôt une façon de mettre à disposition les handlers. J'ai découvert pour la première fois cette stratégie via la librairie msw-ui qui propose un composant de sélection des scénarios. Cependant, la mise en œuvre étant plutôt simple, nous allons voir comment réaliser cette fonctionnalité nous-même et ainsi nous éviter l'ajout d'une énième bibliothèque.

La première modification consiste à ajuster le script mocks/browser que nous avons vu plus haut afin mettre à disposition une nouvelle fonction, applyScenario. Cette dernière va nous permettre d'appliquer le scénario qui sera sélectionné au lancement de l'application. Lors de l'appel à cette fonction, nous allons extraire le scénario passé en paramètre afin de demander au worker de l'utiliser. Mais avant ça, il est nécessaire de faire une réinitialisation des handlers afin de nettoyer les mocks utilisés par le worker.

import { setupWorker } from 'msw';
import { scenarios } from './handlers';

export const worker = setupWorker();

export const applyScenario = (scenarioName: string) => {
    const handlers = scenarios[ scenarioName ];

    if (!handlers) {
        throw new Error(`The scenario "${ scenarioName }" does not exist`);
    }

    worker.resetHandlers();
    worker.use(...handlers);
};
Euh, et ça vient d'où ça scenarios ?

L'objet scenarios est un simple dictionnaire de handlers. Chaque scénario décrit un use case spécifique. Par exemple, le scénario handlersWithEmptyData est un cas d'usage qui va retourner une liste vide afin de visualiser le comportement de notre application dans ces conditions.

import { RestHandler } from 'msw';
import {
	getProductsWithEmptyDataMock,
} from 'services/products/products.mock';

export const handlersByDefault: ReadonlyArray<RestHandler> = [
    // ...
];

export const handlersWithEmptyData: ReadonlyArray<RestHandler> = [
    getProductsWithEmptyDataMock(),
    // ...
];

export const scenarios: Record<string, ReadonlyArray<RestHandler>> = {
    byDefault: handlersByDefault,
    withEmptyData: handlersWithEmptyData,
    // ...
];
Super, j'ai des scénarios. Je fais quoi avec ça maintenant ?

L'intérêt des scénarios est de mettre à disposition des supers utilisateurs (Dev, PO, métier, QA...) un écran de choix des scénarios permettant de sélectionner celui qui est à activer pour la suite de la navigation. Cette mise à disposition peut être réalisée de différentes façons :

  • Un écran de sélection au lancement de l'application activé via un query param (Exemple illustré plus haut),
  • Une commande disponible en console pour changer de scénario,
  • Une pop up qui s'ouvre avec une séquence spécifique de touche (konami code),
  • ...

Les possibilités qui s'offrent à vous sont très vastes et je ne suis pas en mesure de vous fournir une solution magique qui va répondre à tous vos besoins.

Oui d'accord, mais je ne saisis toujours pas l'intérêt des scénarios ?

C'est vrai que dans ce paragraphe, je me suis un peu noyé dans la technique en oubliant de parler de la valeur intrinsèque qu'apporte cette construction.

À mon sens, les scénarios sont parties intégrantes de la documentation et ils ont l'avantage, tout comme les tests, d'être de la documentation dite vivante. En effet, puisqu'ils sont utilisés pour visualiser l'application (et potentiellement dans les tests), ils seront automatiquement maintenus à jour par les développeurs. En ce sens, ils :

  • permettent aux équipes de réalisation de facilement tester les comportements de l'application sans avoir à altérer la base de code,
  • donnent de la visibilité au métier sur une partie des règles de gestion qui sont implémentés dans l'application,
  • facilitent l'onboarding des nouveaux puisque toutes les déclinaisons de l'application sont à portée de clic,
  • rendent compte de la complexité inhérente de l'application en dehors des équipes de réalisation (Top management, sponsors...)
  • ...

Pour toutes ces raisons, je suis intimement persuadé que les scénarios sont un outil puissant qui peut vous aider à décupler la production de valeur des équipes de développement.

Conclusion

J'espère avoir su vous transmettre l'enthousiasme que j'ai ressenti lors de mon premier contact avec msw. Mais aussi que je vous ai donné l'envie d'essayer d'en faire autant de votre coté afin d'expérimenter par vous-même ce que cette bibliothèque peut vous apporter.

J'ai beaucoup apprécié coucher sur le papier l'ensemble des connaissances que j'ai réussi à retirer ce cette expérience avec cette nouvelle bibliothèque. J'espère que vous aurez pris autant de plaisir à me lire que j'en ai pris à écrire cet article.

Postface

Cet article a été écrit avec l'aide d'une IA générative, Bard, le modèle de langage créé par Google AI. C'est une technologie récente et j'ai encore beaucoup de difficulté à mettre correctement en forme mes questions pour obtenir des résultats pertinents. Il m'a surtout aidé à modifier mes tournures de phrase afin d'éviter la trop grande redondance et à trouver de l'inspiration pour créer des paragraphes plus engageants...

Remerciements

Je tiens à remercier mes relecteurs Angélo LIMA, Brice MARCHAND, David ABOULKHEIR, Grégory BARALE et Guillaume FÉLIX pour les retours, les conseils, le soutien et leurs encouragements tout au long du processus de rédaction de cet article.

Merci encore pour votre temps et votre attention.

Dernier