Jest ou les tests JS sans douleur

Lorsque l’on souhaite tester une application web écrite en JavaScript, on part très souvent du principe qu’il va falloir monter une solution de test à partir du runner Karma.
En choisissant Karma, on peut élaborer finement une stack de test, puisque qu’il faut choisir chacune des librairies qui gravitent autour de cette stack (pour définir les tests, faire des assertions, des mocks…).
Seulement, choisir une telle solution demandera de mettre en place une configuration assez complexe qui peut être facilement rebutante si l’on souhaite se lancer dans les tests front-end.

Cependant, depuis 2015, Facebook a mis au point Jest, une solution de test tout en une et fonctionnant intégralement avec NodeJS.
Si Jest a été créé, à la base, pour être une solution de tests ReactJS, sa simplicité d’utilisation et ses performances ont permis de fédérer une importante communauté de développeurs (18.4K d’étoiles sur Github pour 2.3K forks) afin que cet outil puisse être utilisable sur l’ensemble de l’écosystème JavaScript, notamment avec Angular et VueJS.

À travers cet article, nous allons donc voir quels sont les avantages de Jest par rapport à Karma et pour se mettre en condition d'industrialisation, nous allons considérer que les applications testées seront écrites en TypeScript.

Une installation unifiée pour une configuration simplifiée

Gestion des dépendances

Lorsque l’on parle de test avec Karma, le gros point noir auquel on pense immédiatement concerne la mise en place de sa stack. En effet, Karma n’est finalement qu’un élément d’une longue liste de dépendances nécessaires pour mettre en place des tests JS.

Voici un exemple d’une liste de dépendances à posséder pour créer une stack de test Karma fonctionnelle :

/* Les bibliothèques de types TS de chaque dépendances */
"@types/chai": "4.1.2",
"@types/mocha": "2.2.48",
"@types/sinon": "4.1.3",

/* les bibliothèques */
"chai": "4.1.2", // La bibliothèque d'assertion
"karma": "2.0.0", // Le moteur de test
"karma-chrome-launcher": "2.2.0", // Le navigateur de test
"mocha": "5.0.1", // Le framework de tests
"sinon": "4.3.0" // La bibliothèque de mocks
/*****************************************/
/* Les plugins d'intégration de mocha dans karma */
"karma-mocha": "1.3.0",
"karma-mocha-reporter": "2.2.5",
/*****************************************/ 
/* Les plugins pour lancer des tests écrits en Typescript avec Karma */
"karma-sourcemap-loader": "0.3.7",
"karma-webpack": "2.0.11",
/*****************************************/
/* Les plugins pour faire du code coverage */
"karma-remap-istanbul": "0.6.0"
"istanbul-instrumenter-loader": "3.0.1",
/*****************************************/

Comme on peut le voir, 14 dépendances sont nécessaires pour pouvoir correctement faire des tests et du code coverage.

Voici à présent, les dépendances nécessaires pour créer la même stack, avec Jest :

"jest-preset-angular": "5.2.2", // Pour utiliser Jest avec Angular
"vue-typescript-jest": "0.3.0", // Pour utiliser Jest avec VueJS
"@types/jest": "21.1.5",
"jest": "20.0.4",
"ts-jest": "20.0.7" // Pour exécuter des tests TS avec Jest

3 ou 4 dépendances, (dont 2 pour gérer TS avec Jest) et c’est tout. Jest inclut tous les outils pour créer une stack basique et efficace (comprenant le moteur, le framework, le système d’assertions et de mocks avec également le code coverage) ce qui rend votre système de test bien plus simple à maintenir.

Configuration de l’outil

Bien évidemment, on retrouve cette simplicité de Jest au niveau de sa configuration.

Reprenons la stack Karma que nous avons déclarée précédemment. Pour la faire fonctionner, il va falloir faire de la configuration sur 3 fichiers différents :

  • Un fichier pour déclarer l’environnement Webpack utilisé pour les tests (pour gérer la compilation TS) :
module.exports  = {
	devtool:  'inline-source-map',
	resolve: {
	extensions: ['.ts', '.tsx', '.js']
	},
	module: {
		rules: [
			{
				test: /\.(ts|tsx)$/,
				loader:  require.resolve('tslint-loader'),
				enforce:  'pre',
				include: './src',
			},
			{
				test: /\.(ts|tsx)$/,
				include:  './src',
				loader:  require.resolve('ts-loader'),
			},
			{
				test: /\.(ts|tsx)$/,
				enforce:  'post',
				loader:  'istanbul-instrumenter-loader',
				exclude: /(node_modules|test)/
			},

			{
				test: /\.js$/,
				exclude: /\/node_modules\//,
				loader:  'babel',
			},
			{
				test: /\.css$/,
				use: [
					require.resolve('style-loader'),
					{
                        loader:  require.resolve('css-loader'),
						options: {
							importLoaders:  1,
						},
					}
				],
			},
		],
	}
}
  • Un fichier pour déclarer la configuration de Karma :
const  webpackConfiguration  =  require('./config/webpack.config.test');

module.exports  =  function (config) {
	config.set({
		frameworks: ['mocha'],
		browsers: ['Chrome'],
		files: [
			'src/setupTests.tsx'
		],
		preprocessors: {
			'src/setupTests.tsx': ['webpack', 'sourcemap']
		},
		webpack:  webpackConfiguration,
		reporters: ['mocha', 'karma-remap-istanbul'],
		singleRun:  false,
		remapIstanbulReporter: {
			reports: {
				lcovonly:  './coverage/lcov.info'
			}
		}
	});
};
  • Un fichier pour déclarer le point d’entrée des tests :
const  testsContext  =  require.context('.', true, /.test$/);
testsContext.keys().forEach(testsContext);

Comme vous pouvez le constater, mettre en place la configuration nécessaire pour lancer votre stack Karma peut se révéler très complexe.

Heureusement, il est bien plus simple de configurer Jest. Pour cela, vous pouvez au choix déclarer votre configuration directement dans votre package.json ou bien dans un fichier à part (il faudra alors indiquer à Jest son fichier de configuration lors de son lancement) :

{
	testMatch: [
		'<rootDir>/test/**/*.test.ts?(x)'
	],
	collectCoverage:  true,
	collectCoverageFrom: [
		'<rootDir>/src/**/*.{ts,tsx}',
	],
	coverageReporters: ['lcov'],
	transform: {
		'^.+\\.(ts|tsx)$':  'ts-jest',
        //On mock les fichiers tierces
		'^(?!.*\\.(js|jsx|css|json)$)':  '<rootDir>/fileTransform.js' 
	},
	transformIgnorePatterns: [
		'[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$'
	],
	moduleFileExtensions: [
	'ts',
	'tsx',
	'json',
	'js',
	'jsx',
	'node'
	]
};

En bonus, on fournit le contenu du fichier fileTransform.js :

'use strict';
const  path  =  require('path');
// This is a custom Jest transformer turning file imports into filenames.
// http://facebook.github.io/jest/docs/tutorial-webpack.html
module.exports  = {
	process(src, filename) {
		return  `module.exports = ${JSON.stringify(path.basename(filename))};`;
	},
};

Voilà, en quelques lignes on obtient un environnement de test prêt à l’emploi !

Si l’on a pris le temps de comparer Karma et Jest au niveau de la gestion de leurs dépendances et de leur configuration, c’est surtout pour montrer qu’il est tout à fait possible de mettre en place un système de test JS à moindre coût.
Par conséquent, si vous avez peur de perdre du temps sur votre projet en mettant en place une stack de test, Jest peut alors être une bonne solution.

Cas de la migration vers Jest sur un projet existant

Prenons un exemple de test écrit avec la stack Karma (et Chai pour les assertions) :

import { expect } from  'chai';

describe('MyFunction', () => {
	it('should do a sum', () => {
		const mySumFunction = (a, b) => a + b;
		expect(mySumFunction(2,2)).to.equal(4);
	});
});	

Maintenant le même test écrit pour Jest :

describe('MyFunction', () => {
	it('should do a sum', () => {
		const mySumFunction = (a, b) => a + b;
		expect(mySumFunction(2,2)).toEqual(4);
	});
});	

En regardant cet exemple, on pourrait se dire que passer de Karma à Jest requiert une réécriture de vos tests. Sauf qu’il n’en est rien.
En effet, il est tout à fait possible de faire cohabiter vos bibliothèques de tests tierces (par exemple votre bibliothèque d’assertion ou de mock) avec Jest. Vous pourrez alors passer facilement à Jest tout en conservant l’écriture que vous avez adoptée pour vos tests Karma.

De meilleures performances

L’un des gros apports de Jest par rapport à Karma concerne la vitesse d’exécution des tests répartis sur plusieurs fichiers.
Habituellement lors d’un test Karma, on crée un bundle de tests à partir de l’intégralité des fichiers. Une fois le bundle constitué, les tests sont exécutés un à un.

const  testsContext  =  require.context('.', true, /.test$/);
testsContext.keys().forEach(testsContext);

Jest est, lui, capable de les exécuter fichier par fichier tout en parallélisant leur traitement.
Pour constater la différence, on peut tout simplement utiliser un projet généré avec JHipster.
Avec JHipster (5.0.0.beta / React), un projet fraichement généré contient approximativement 80 tests répartis sur une dizaine de fichiers.
En lançant les tests avec Karma, ces derniers sont compilés et exécutés en 20 secondes. Si on reprend ce même projet et qu’on le migre vers Jest (vous trouverez le projet migré ici), le temps de traitement total chute à 5 secondes.

Image Description

Ces chiffres sont bien évidemment amenés à bouger en fonction de votre machine mais on peut légitimement penser que Jest sera à chaque fois le plus rapide.
Si ce gain de temps se fera ressentir, par exemple lors de votre processus d’intégration continue, l'exécution des tests fichiers par fichiers sera surtout utile lors de vos phases de développement quotidiennes.
En effet, si vous souhaitez exécuter tout ou partie de vos tests avec Karma, celui-ci créera à chaque fois un bundle complet (sauf si vous modifiez la méthode de génération du bundle, ce qui rend le test d’un échantillon peu pratique au quotidien). Alors qu’avec Jest, la sélection (avec la fonctionnalité de filtrage que nous verrons plus bas) et surtout l'exécution des tests est grandement simplifiée puisqu’il est capable de ne sélectionner que les fichiers concernés.

Les snapshots, un outil très pratique pour la détection de régressions

Avec l’émergence des nouvelles technologies web comme Angular, React ou Vue, il arrive assez souvent que l’on veuille écrire notre application sous la forme d’une imbrication de composants. C’est pourquoi Jest vient avec un nouvel outil : le test par snapshot (ou snapshot testing).
Son principe est simple : on déclare un test dans lequel l’outil de snapshot va capturer un composant et sauvegarder son DOM dans un fichier.

Un exemple (il est écrit en React mais son fonctionnement est le même sur Angular et Vue) :

it('should render a div', () => {
	const  componentTree  =  renderer.create(
        <div className=".divDeTest">maDivDeTest</div>
    ).toJSON();
    
	// Jest's expect function
	expect(componentTree).toMatchSnapshot();
});

Lors du premier passage, un fichier .snap va être créé dans lequel on va retrouver le DOM de notre composant :

exports[`<TodoList /> should render a div 1`] = `
<div
  className=".divDeTest"
>
  maDivDeTest
</div>
`;

Pour les passages suivants, le test comparera alors le DOM du composant instancié dans la variable componentTree et le DOM enregistré dans le fichier .snap. Si les deux DOM sont identiques alors le test sera validé, sinon le test sera mis en erreur et le client Jest vous demandera si le snapshot doit être mis à jour.

snpashot_summary

L’idée derrière le snapshot testing est tout simplement d’offrir un outil facile à mettre en place et qui puisse être le témoin de l’évolution de votre application.

Si l’on peut voir le snapshot testing comme étant un bon complément aux tests métiers de votre composant, cette fonctionnalité a surtout été conçue pour les développeurs qui ont peu de temps à consacrer aux tests. Plutôt que de ne faire aucun test, il vaut mieux mettre en place ce système sur chacun des composants puisqu’il vous aidera à détecter plus facilement de potentielles régressions.

Point important à noter sur le snapshot testing : de base cette fonctionnalité n’est disponible que pour du développement React. Cependant, grâce à la communauté derrière Jest, on peut utiliser des plugins pour rendre le snapshot testing compatible avec Angular et Vue.

Un client de test plus ergonomique

Pour finir ce tour d’horizon de Jest, on va s’attarder sur deux fonctionnalités concernant son client.

Filtrage des tests

tests_result

Lorsqu’on lance Jest, ce dernier propose plusieurs options pour filtrer les tests à exécuter. Parmi les modes de filtrage disponibles, on trouve :

  • Un mode pour n’exécuter que les tests échoués.
  • Un mode pour n’exécuter que les fichiers de tests modifiés (ce mode a besoin de Git pour détecter les fichiers modifiés).
  • Un mode pour filtrer les tests en fonction de leurs fichiers.
  • Un mode pour filtrer les tests en fonction de leurs noms.

Si tous ces modes vont vous aider à exécuter rapidement les tests dont vous avez besoin, les deux derniers vous seront très utiles si vous décidez d’adopter, par exemple, une démarche Test Driven Development pour la création de votre application.

search_result

Une fois que vous aurez écrit vos tests pour une fonction ou un composant, il vous suffira alors de lancer Jest en précisant les noms des tests ou leurs fichiers pour que vous puissiez profiter d’un rafraîchissement automatique et rapide tout au long de votre développement.

Un retour de tests plus lisible

Cela peut paraître relever du détail, mais avoir une stacktrace lisible peut se révéler pratique si l’on ne souhaite pas perdre de temps pour comprendre la cause des tests en erreur.

Sous Karma, les reporters disponibles nous renvoient généralement une stacktrace de cette forme :

karma_stack_trace

Si la stacktrace indique clairement le test et la ligne qui pose problème, celle proposée par Jest affiche bien plus d’informations tout en ayant une mise en page plus lisible. Jest se paie même le luxe d’afficher l’extrait du code qui pose soucis ! Autant dire que grâce à ce système, vous serez bien plus efficaces pour analyser vos tests en échec.

jest_stack_trace

Quid du test de compatibilité ?

Arrivé à la fin de cet article, on pourrait légitimement se dire que Jest est une solution qui surpasse en tout point Karma. Seulement, lorsque l’on veut tester du code JavaScript, il arrive quelquefois que l’on veuille également s’assurer qu’il puisse être compatible avec l’ensemble des navigateurs du marché.

Si Karma permet de réaliser ce test de compatibilité (on peut, par exemple, exécuter les tests dans un Chrome ou un Firefox headless), il n’est pas possible de faire de même dans Jest. En effet, les tests étant exécutés dans JSDOM (un mock de navigateur pour Node.JS), on ne peut pas savoir si le code sera défaillant sur un navigateur donné.
C’est pour cela qu’avant de choisir une solution de test, il est indispensable d’évaluer si cette information de compatibilité vous sera utile.

Si vous comptez travailler avec des frameworks populaires (Angular, Vue, React, Ionic...) il y a de grandes chances pour que le scope de compatibilité soit clairement défini (ex: Le guide de compatibilté Angular), donc vérifier la compatibilité de votre application ne sera pas nécessairement utile.
En revanche, si vous souhaitez développer une application avec du JS classique ou que vous souhaitez inclure des fonctions expérimentales, passer à Jest ne sera pas nécessairement la meilleure des solutions. C’est pourquoi, il sera toujours important de bien définir vos besoins avant de choisir une solution de test.

Conclusion

Avec Jest, l’idée est avant tout de vous proposer un nouvel outil pour vous permettre, à la fois, d’affiner vos choix techniques lorsque vous devrez mettre en place une application front-end et aussi pour vous aider à systématiser plus facilement l’usage des tests JS.

Si vous souhaitez creuser le sujet, voici quelques liens pour vous aider à utiliser Jest sur vos projets :

Des exemples d’intégration de Jest :

Ou plus simplement, vous pouvez aller voir du coté de JHipster qui intègre Jest comme outil de test front depuis sa version 5.