JS monorepos en prod 4 : tests unitaires avec Mocha et Should.js
By WORMS David
25 févr. 2021
Ne ratez pas nos articles sur l'open source, le big data et les systèmes distribués, fréquence faible d’un email tous les deux mois.
Les tests unitaires sont cruciaux pour tous les projets à long terme et permettent d’isoler des fonctionnalités de votre code en unités testables. En effet, l’objectif principal des tests unitaires est de pouvoir isoler les bugs en vérifiant que certaines portions indépendantes de notre code fonctionnent correctement. Les test unitaires doivent donc être écrits de manière à la fois précises et exhaustive pour couvrir un maximum de situations.
Dans cet article nous couvrirons et comparerons les tests unitaires à la fois dans le langage JavaScript et CoffeeScript. Pour cela nous utiliserons le très populaire framework de test Mocha en combinaison avec la bibliothèque Should.js. Dans le prochain article de notre série, nous discuterons de l’intégration CI/CD dans le contexte des monorepos :
- Partie 1 : initialisation de project
- Partie 2 : gestion des versions et de la publication
- Partie 3 : messages de commit et génération du changelog
- Partie 4 : tests unitaires
- Partie 5 : fusion de plusieurs dépôts Git et préservation des commits
- Partie 6 : CI/CD, intégration et déploiement continus avec Travis CI
- Partie 7 : CI/CD, intégration et déploiement continus avec GitHub Actions
Tests unitaires avec Mocha and Should.js
Alors que le package gatsby-remark-title-to-frontmatter
a été utilisé pendant un certain temps sur plusieurs de nos sites Web et pourrait donc être considéré comme stable, il semble difficile de publier une version stable 1.0.0
sans aucun tests unitaires.
J’ai pour habitude de commencer mes projets par l’écriture de tests unitaires avant même de commencer tout développement. Contre-productif, perte de temps ? Pas vraiment. Cette pratique apporte des avantages significatifs sur le long terme. En effet, couvrir votre base de code avec un ensemble complet de tests unitaires aidera définitivement à garder votre code propre et fonctionnel et facilitera tout processus de réécriture du code.
Comme indiqué précédemment, nous allons utiliser le framework Mocha pour les tests unitaires et Should.js pour les assertions. Mais n’hésitez pas à utiliser vos outils préférés. En effet, il existe plusieurs bibliothèques et outils alternatifs dont Jest et Chai.
Avant de commencer, déplaçons nous d’abord dans le répertoire gatsby-remark/title-to-frontmatter
:
cd gatsby-remark/title-to-frontmatter
Ensuite, nous devons ajouter Mocha et Should.js à nos dépendances :
yarn add -D mocha should
La commande yarn
a été exécutée avec quelques arguments. L’argument -D
ou --dev
permet d’installer un ou plusieurs packages dans devDependencies
du ficher packages.json
que l’on peut trouver dans notre package gatsby-remark/title-to-frontmatter
.
De plus, il serait pratique de modifier la configuration de Mocha de telle sorte qu’à chaque fois que vous importez ce package il puisse automatiquement importer la bibliothèque d’assertions Should.js. Il y a plusieurs endroits où vous pouvez définir cette configuration. Personnellement je modifie le fichier package.json
comme ci-dessous :
{
...
"mocha": {
"throw-deprecation": true,
"require": [
"should"
],
"inline-diffs": true,
"timeout": 40000,
"reporter": "spec",
"recursive": true
}
}
Maintenant, Should.js sera automatiquement importe avec mocha
grâce à la propriété require
définie dans package.json
. Ainsi nous n’aurons, par conséquence, aucunement besoin de l’importer dans nos modules de test.
Les tests unitaires prennent la forme de fonctions écrite par le dévelopeur. Dans Mocha, on passe cette fonction en argument de it
. Les fonctions de test sont regroupées au sein de la fonction describe
. Un test doit couvrir une fonctionnalité de votre code de la manière la plus lisible et la plus précise. Lorsque cela est possible, ils doivent être autonomes sans nécessiter de données externes. Au contraire, ils doivent recréer les conditions de leur réussite. Je préfère tout avoir dans mes fonctions de test et éviter les fixtures stockés dans un endroit différent. Cependant, chaque projet a ses propres spécificités et cette approche n’est pas toujours possible. Néanmoins, lorsque cela l’est, je trouve extrêmement pratique d’avoir une vue globale sur les paramètres et le sujet testés ainsi que l’assertion de sortie.
Pour illustrer cela, nous utilisons le package title-to-frontmatter
créé dans le premier article de cette série. Brièvement, title-to-frontmatter
analyse un document Markdown, en supprime le titre puis le place à l’intérieur de l’objet frontmatter
. Le code est situé dans le répertoire ./packages/title-to-frontmatter
.
Afin de tester le module “./lib/index.js”, nous écrivons un test dans le fichier test/index.js
utilisant Mocha et Should.js. Le test ressemble à ceci :
const Remark = require('remark')
const toHtml = require('hast-util-to-html')
const toHast = require('mdast-util-to-hast')
const extractTitle = require('..')
describe( 'Extract title', () => {
it( 'Move the title to frontmatter', () => {
// Initialize
const mast = (new Remark()).parse(
[
'# this is the title',
'and some text'
].join('\n')
)
const frontmatter = {}
// Run
extractTitle({
markdownNode: {
frontmatter: frontmatter
},
markdownAST: mast
}, {})
// Convert
const hast = toHast(mast)
const html = toHtml(hast)
// Assert
html.should.eql('<p>and some text</p>')
frontmatter.should.eql({
title: 'this is the title'
})
})
})
Mais avant d’exécuter le test, ajoutons d’abord les dépendances nécessaires :
yarn add -D \
remark \
hast-util-to-html \
mdast-util-to-hast
Nous pouvons ensuite exécuter notre test :
yarn mocha test/index.js
yarn run v1.22.5
$ /Users/david/projects/github/remark-gatsby-plugins/node_modules/.bin/mocha test/index.js
Extract title
✓ Move the title to frontmatter
1 passing (13ms)
✨ Done in 0.56s.
Le test s’est exécuté avec succès. Nous pouvons donc enregistrer les nouvelles modifications dans le dépôt :
git add package.json test/index.js
git commit -m "test(gatsby-remark-title-to-frontmatter): move the title to frontmatter"
Structure de projet
Habituellement, il est recommandé d’aligner la structure de votre dossier de test avec la structure de votre code source. Il existe principalement trois stratégies pour placer vos tests.
Certains développeurs incluent leurs tests directement dans le code source cible. Les tests peuvent s’exécuter sous certaines conditions comme avec la présence d’une variable d’environnement ou lorsque le module est directement exécuté au lieu d’être importé.
Il est aussi possible de placer les tests à côté du module testé. En utilisant une convention de nommable du type ./lib/my_module.test.js
, une expression de glob comme node lib/**/*.test.*
n’exécutera que les tests unitaires.
Finalement, mon approche favorite, utilisée plus haut, consiste à placer les tests dans un répertoire dédié comme ./test
. Le test ./test/my_module.js
testera le module ./lib/my_module.js
. Si plusieurs fonctionnatilé d’une même module sont testé, il est aussi possible d’organiser tous les tests dans un même répertoire, ./test/my_module
par exemple.
Tests unitaires avec CoffeeScript
Personnellement, je préfère utiliser CoffeeScript lors de l’écriture de mes tests. C’est l’approche que j’ai adoptée pour le package CSV parser. Le code source est écrit en JavaScript tandis que les tests sont en CoffeeScript. Cela rend le code des tests unitaires plus courts, plus lisibles et beaucoup plus expressifs. Intégrons tout d’abord CoffeeScript dans notre package avant d’ensuite convertir notre test :
yarn add -D coffeescript
Dans la configuration Mocha présente dans le fichier package.json
, ajoutez coffeescript/register
:
{
"mocha": {
"throw-deprecation": true,
"require": [
"should",
"coffeescript/register" ],
"inline-diffs": true,
"timeout": 40000,
"reporter": "spec",
"recursive": true
}
}
De manière similaire à ce que nous avons fait précédemment avec Should.js, CoffeeScript est maintenant enregistré dans Mocha. De cette façon, le code sera automatiquement convertit de CoffeeScript vers JavaScript avant d’être exécuté par le moteur de Node.js. Maintenant nous pouvons réécrire notre test.
Remark = require 'remark'
toHtml = require 'hast-util-to-html'
toHast = require 'mdast-util-to-hast'
extractTitle = require '..'
describe 'Extract title', ->
it 'Move the title to frontmatter', ->
# Initialize
mast = (new Remark()).parse """
# this is the title
and some text
"""
frontmatter = {}
# Run
extractTitle
markdownNode:
frontmatter: frontmatter
markdownAST: mast
, {}
# Convert
hast = toHast mast
html = toHtml hast
# Assert
html.should.eql '<p>and some text</p>'
frontmatter.should.eql
title: 'this is the title'
Je laisse le lecteur juger de l’esthétique et de l’expressivité de CoffeeScript. Les goûts et les couleurs ne se choisissent pas selon des critères rationnels !
Pour exécuter tous les tests, nous pouvons maintenant enregistrer la commande yarn test
dans le fichier package.json
. En supposant que les tests sont écrits en CoffeeScript (sinon changez l’extension .coffee
en .js
dans l’expression de glob), la commande ressemble à ceci :
{
"scripts": {
"test": "mocha 'test/**/*.coffee'"
}
}
L’exécution de la commande yarn test
(ou npm test
) produit le même résultat qu’avec la commande mocha
:
yarn test
yarn run v1.22.5
$ mocha 'test/**/*.coffee'
Extract title
✓ Move the title to frontmatter
1 passing (11ms)
✨ Done in 0.83s.
Très bien, nous pouvons maintenant enregistrer les nouvelles modifications dans le dépôt :
git rm test/index.js
git add package.json test/index.coffee
git commit -m "test(gatsby-remark-title-to-frontmatter): convert test to coffee"
Exploiter lerna
lors des test unitaires
Jusqu’à présent nous avons seulement décrit comment lancer les test indépendamment dans chaque package. Lancer manuellement chaque test dans chaque package est chronophage et sujet à erreur, notamment si votre projet possède un grande nombre de package et modules à tester. L’intérêt de Lerna est de pouvoir utiliser une unique commande pour lancer l’ensemble de nos tests avec la commande lerna run
ci-dessous :
lerna run <script> -- [...args]
Cette commande permet de lancer des scripts NPM dans chacun de nos packages qui contient ledit script. Ainsi nous pouvons utiliser cette commande pour lancer les scripts de test. Pour rappel, notre package contient l’entrée script.test
dans le fichier package.json
:
{
...
"scripts": {
"test": "mocha 'test/**/*.coffee'"
}
}
Par conséquent nous pouvons utiliser la commande suivante pour lancer les tests :
lerna run test
info cli using local version of lerna
lerna notice cli v3.22.1
lerna info versioning independent
lerna info Executing command in 1 package: "yarn run test"
lerna info run Ran npm script 'test' in 'gatsby-remark-title-to-frontmatte
r' in 0.5s:
yarn run v1.22.10
$ mocha 'test/**/*.coffee'
Extract title
✓ Move the title to frontmatter
1 passing (8ms)
Done in 0.41s.
lerna success run Ran npm script 'test' in 1 package in 0.5s:
lerna success - gatsby-remark-title-to-frontmatter
Comme vous pouvez le voir la commande lerna run test
va chercher dans notre package le script de test et le lance. Dans notre package le script permet de lancer la commande mocha 'test/**/*.coffee'
comme décrit précédemment. Ainsi cette approche permettra de pouvoir lancer l’ensemble des tests présents dans votre projet assumant que les fichiers package.json
incluent un script NPM "test"
.
Pour les plus minimalistes d’entre nous, nous pouvons affiner notre approche et éviter de devoir lancer systématiquement la commande lerna run test
. Pour cela modifions le fichier package.json
présent à la racine de notre projet :
{
"scripts": {
"postinstall": "husky install",
"publish": "lerna publish from-git --yes",
"test": "lerna run test" }
}
Une fois les modifications ci-dessus appliquées, nous utilisons la commande yarn test
pour lancer l’ensemble de nos tests.
Aide-mémoire
- Installation de Mocha et de Should.js :
yarn add -D mocha should # If using CoffeeScript yarn add -D coffeescript
- Configuration de Mocha dans
package.json
:{ "mocha": { "throw-deprecation": true, "require": [ "should", "coffeescript/register" ], "inline-diffs": true, "timeout": 40000, "reporter": "spec", "recursive": true } }
- Template Mocha en JavaScript :
describe( 'Group description', () => { it( 'Test description', () => { // Write your test }) })
- Template Mocha en Coffee :
describe 'Group description', -> it 'Test description', -> # Write your test
- Commande
test
danspackage.json
:{ "scripts": { "test": "mocha 'test/**/*.coffee'" } }
- Commande
test
pour exécuter tous les packages avec Lerna :{ "scripts": { "test": "lerna run test" } }
Conclusion
Nous avons brièvement décrit certaines bonnes pratiques lors de l’utilisation des tests unitaires avec le framework Mocha et Should.js. Nous avons montré comment écrire des tests à la fois en JavaScript et CoffeeScript. Pour utiliser ce dernier ajoutez seulement le package approprié et configurez Mocha dans le fichier package.json
. Hormis quelques différences esthétiques et expressives entre les tests d’écriture en JavaScript et en CoffeeScript, les deux langages restent similaires. Cependant, j’apprécie l’expressivité et la lisibilité de CoffeeScript et par conséquent il reste mon langage de prédilection pour l’écriture de tests. Enfin, gardez à l’esprit que l’écriture et la configuration de tests unitaires dans les monorepos ne sont pas différents de ceux utilises avec des repos plus classiques. Assurez-vous simplement que chaque dossier de test reste dans le bon package et pensez à exploiter la commande learn run test
comme décrit dans cet article pour pouvoir lancer l’ensemble des tests présents dans votre monorepos.
Dans notre prochain article nous verrons que bien que la mise en œuvre de test en monorepos soit plutôt aisée, l’intégration du pipeline CI/CD pour automatiser la publication des packages nécessite, par contre, quelques paramétrages supplémentaires que nous couvrirons de manière exhaustive.