Comment démarrer le test unitaire de votre code JavaScript

Nous savons tous que nous devrions écrire des tests unitaires. Mais il est difficile de savoir par où commencer et combien de temps consacrer aux tests par rapport à la mise en œuvre réelle. Alors, par où commencer? Et s'agit-il simplement de tester du code ou les tests unitaires ont-ils d'autres avantages?

Dans cet article, j'expliquerai les différents types de tests et quels avantages les tests unitaires apportent aux équipes de développement. Je vais présenter Jest - un cadre de test JavaScript.

Différents types de tests

Avant de plonger dans les spécificités des tests unitaires, je souhaite faire un rapide tour d'horizon des différents types de tests. Il y a souvent une certaine confusion autour d'eux et je ne suis pas surpris. Parfois, la ligne entre eux est assez mince.

Tests unitaires

Les tests unitaires ne testent qu'une seule partie de votre implémentation. Une unité. Pas de dépendances ou d'intégrations, pas de spécificités du framework. Ils sont comme une méthode qui renvoie un lien dans une langue spécifique:

export function getAboutUsLink(language){ switch (language.toLowerCase()){ case englishCode.toLowerCase(): return '/about-us'; case spanishCode.toLowerCase(): return '/acerca-de'; } return ''; }

Tests d'intégration

À un moment donné, votre code communique avec une base de données, un système de fichiers ou un autre tiers. Cela pourrait même être un autre module de votre application.

Cet élément d'implémentation doit être testé par des tests d'intégration. Ils ont généralement une configuration plus compliquée qui implique la préparation d'environnements de test, l'initialisation des dépendances, etc.

Tests fonctionnels

Les tests unitaires et les tests d'intégration vous donnent l'assurance que votre application fonctionne. Les tests fonctionnels examinent l'application du point de vue de l'utilisateur et vérifient que le système fonctionne comme prévu.

Dans le diagramme ci-dessus, vous voyez que les tests unitaires constituent la grande base de la suite de tests de votre application. Généralement, ils sont petits, ils sont nombreux et ils sont exécutés automatiquement.

Passons maintenant un peu plus en détail aux tests unitaires.

Pourquoi devrais-je prendre la peine d'écrire des tests unitaires?

Chaque fois que je demande aux développeurs s'ils ont écrit des tests pour leur application, ils me disent toujours: "Je n'ai pas eu le temps pour eux" ou "Je n'en ai pas besoin, je sais que ça marche".

Alors je souris poliment et leur dis ce que je veux vous dire. Les tests unitaires ne concernent pas uniquement les tests. Ils vous aident également d'autres manières, afin que vous puissiez:

Soyez sûr que votre code fonctionne. À quand remonte la dernière modification du code, votre build a échoué et la moitié de votre application a cessé de fonctionner? Le mien était la semaine dernière.

Mais c'est toujours OK. Le vrai problème est lorsque la génération réussit, que la modification est déployée et que votre application commence à être instable.

Lorsque cela se produit, vous commencez à perdre confiance en votre code et finalement vous priez simplement pour que l'application fonctionne. Les tests unitaires vous aideront à découvrir les problèmes beaucoup plus tôt et à gagner en confiance.

Prenez de meilleures décisions architecturales. Changements de code, mais certaines décisions concernant la plate-forme, les modules, la structure et autres doivent être prises au cours des premières étapes d'un projet.

Lorsque vous commencez à penser aux tests unitaires dès le début, cela vous aidera à mieux structurer votre code et à séparer correctement les préoccupations. Vous ne serez pas tenté d'assigner plusieurs responsabilités à des blocs de code unique, car ce serait un cauchemar pour les tests unitaires.

Identifiez les fonctionnalités avant le codage. Vous écrivez la signature de la méthode et commencez à l'implémenter immédiatement. Oh, mais que doit-il se passer si un paramètre est nul? Que faire si sa valeur est en dehors de la plage attendue ou contient trop de caractères? Lancez-vous une exception ou renvoyez-vous null?

Les tests unitaires vous aideront à découvrir tous ces cas. Examinez à nouveau les questions et vous verrez que c'est exactement ce qui définit vos cas de test unitaires.

Je suis sûr que l'écriture de tests unitaires présente de nombreux avantages. Ce ne sont que ceux dont je me souviens de mon expérience. Ceux que j'ai appris à la dure.

Comment rédiger votre premier test unitaire JavaScript

Mais revenons à JavaScript. Nous commencerons par Jest, qui est un framework de test JavaScript. C'est un outil qui permet des tests unitaires automatiques, fournit une couverture de code et nous permet de simuler facilement des objets. Jest a également une extension pour Visual Studio Code disponible ici.

Il existe également d'autres frameworks, si cela vous intéresse, vous pouvez les consulter dans cet article.

npm i jest --save-dev 

Utilisons la méthode mentionnée précédemment getAboutUsLinkcomme implémentation que nous voulons tester:

const englishCode = "en-US"; const spanishCode = "es-ES"; function getAboutUsLink(language){ switch (language.toLowerCase()){ case englishCode.toLowerCase(): return '/about-us'; case spanishCode.toLowerCase(): return '/acerca-de'; } return ''; } module.exports = getAboutUsLink; 

Je mets ça dans le index.jsdossier. Nous pouvons écrire des tests dans le même fichier, mais une bonne pratique consiste à séparer les tests unitaires dans un fichier dédié.

Les modèles de dénomination courants incluent {filename}.test.jset {filename}.spec.js. J'ai utilisé le premier index.test.js,:

const getAboutUsLink = require("./index"); test("Returns about-us for english language", () => { expect(getAboutUsLink("en-US")).toBe("/about-us"); }); 

Tout d'abord, nous devons importer la fonction que nous voulons tester. Chaque test est défini comme une invocation de la testfonction. Le premier paramètre est le nom du test pour votre référence. L'autre est une fonction fléchée où nous appelons la fonction que nous voulons tester et spécifions le résultat attendu. je

Dans ce cas, nous appelons getAboutUsLinkfunction with en-UScomme paramètre de langage. Nous nous attendons à ce que le résultat soit /about-us.

Nous pouvons maintenant installer la CLI Jest globalement et exécuter le test:

npm i jest-cli -g jest 

Si vous voyez une erreur liée à la configuration, assurez-vous que votre package.jsonfichier est présent. Si vous ne le faites pas, générez-en un en utilisant npm init.

Vous devriez voir quelque chose comme ceci:

 PASS ./index.test.js √ Returns about-us for english language (4ms) console.log index.js:15 /about-us Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 2.389s 

Bon travail! Il s'agissait du premier test unitaire JavaScript simple du début à la fin. Si vous avez installé l'extension Visual Studio Code, il exécutera des tests automatiquement une fois que vous aurez enregistré un fichier. Essayons-le en étendant le test avec cette ligne:

expect(getAboutUsLink("cs-CZ")).toBe("/o-nas"); 

Une fois le fichier enregistré, Jest vous informera que le test a échoué. Cela vous aide à découvrir les problèmes potentiels avant même de valider vos modifications.

Test des fonctionnalités avancées et des services de simulation

Dans la vraie vie, les codes de langue de la méthode getAboutUsLink ne seraient pas des constantes dans le même fichier. Leur valeur est généralement utilisée tout au long du projet afin qu'ils soient définis dans leur propre module et importés dans toutes les fonctions qui les utilisent.

import { englishCode, spanishCode } from './LanguageCodes' 

You can import these constants into the test the same way. But the situation will get more complicated if you're working with objects instead of simple constants. Take a look at this method:

import { UserStore } from './UserStore' function getUserDisplayName(){ const user = UserStore.getUser(userId); return `${user.LastName}, ${user.FirstName}`; } 

This method uses imported UserStore:

class User { getUser(userId){ // logic to get data from a database } setUser(user){ // logic to store data in a database } } let UserStore = new User(); export { UserStore } 

In order to properly unit test this method, we need to mock UserStore. A mock is a substitute for the original object. It allows us to separate dependencies and real data from the tested method's implementation just like dummies help with crash tests of cars instead of real people.

If we didn't use the mock, we'd be testing both this function and the store. That would be an integration test and we would likely need to mock the used database.

Mocking a Service

To mock objects, you can either provide a mocking function or a manual mock. I will focus on the latter as I have a plain and simple use-case. But feel free to check out other mocking possibilities Jest provides.

jest.mock('./UserStore', () => ({     UserStore: ({         getUser: jest.fn().mockImplementation(arg => ({             FirstName: 'Ondrej',             LastName: 'Polesny'         })), setUser: jest.fn()     }) })); 

First, we need to specify what are we mocking - the ./UserStore module. Next, we need to return the mock that contains all exported objects from that module.

In this sample, it's only the User object named UserStore with the function getUser. But with real implementations, the mock may be much longer. Any functions you don't really care about in the scope of unit testing can be easily mocked with jest.fn().

The unit test for the getUserDisplayName function is similar to the one we created before:

test("Returns display name", () => {     expect(getUserDisplayName(1)).toBe("Polesny, Ondrej"); }) 

As soon as I save the file, Jest tells me I have 2 passing tests. If you're executing tests manually, do so now and make sure you see the same result.

Code Coverage Report

Now that we know how to test JavaScript code, it's good to cover as much code as possible with tests. And that is hard to do. In the end, we're just people. We want to get our tasks done and unit tests usually yield an unwanted workload that we tend to overlook. Code coverage is a tool that helps us fight that.

Code coverage will tell you how big a portion of your code is covered by unit tests. Take for example my first unit test checking the getAboutUsLink function:

test("Returns about-us for english language", () => {    expect(getAboutUsLink("en-US")).toBe("/about-us"); }); 

It checks the English link, but the Spanish version stays untested. The code coverage is 50%. The other unit test is checking the getDisplayName function thoroughly and its code coverage is 100%. Together, the total code coverage is 67%. We had 3 use cases to test, but our tests only cover 2 of them.

To see the code coverage report, type the following command into the terminal:

jest --coverage 

Or, if you're using Visual Studio Code with the Jest extension, you can run the command (CTRL+SHIFT+P) Jest: Toggle Coverage Overlay. It will show you right in the implementation which lines of code are not covered with tests.

By running the coverage check, Jest will also create an HTML report. Find it in your project folder under coverage/lcov-report/index.html.

Now, I don't have to mention that you should strive for 100% code coverage, right? :-)

Summary

In this article, I showed you how to start with unit testing in JavaScript. While it's nice to have your code coverage shine at 100% in the report, in reality, it's not always possible to (meaningfully) get there. The goal is to let unit tests help you maintain your code and ensure it always works as intended. They enable you to:

  • clearly define implementation requirements,
  • better design your code and separate concerns,
  • discover issues you may introduce with your newer commits,
  • and give you confidence that your code works.

The best place to start is the Getting started page in the Jest documentation so you can try out these practices for yourself.

Do you have your own experience with testing code? I'd love to hear it, let me know on Twitter or join one of my Twitch streams.