Tests unitaires avec Vue.js et Jest
Introduction
Ce tutoriel est un introduction aux tests unitaires avec Vue.js et jest. Nous y verrons les bases de l'écriture de tests avec la vérification du contenu du DOM, le test des évènements, les fonctions asynchrones et enfin le mocking du store Vuex.
Creation d'un projet
Créons un project vue avec l'option extended
vue create demo-unit-tests
Ensuite selectionner le preset extended
Vue CLI v4.5.2
? Please pick a preset:
Default ([Vue 2] babel, eslint)
❯ extended ([Vue 2] dart-sass, babel, typescript, router, vuex, eslint, unit-jest, e2e-cypress)
Default (Vue 3 Preview) ([Vue 3] babel, eslint)
Manually select features
Ce preset contient notament unit-jest qui va nous permettre de tester nos composants.
vue create
génère automatiquement une arboresence de fichier avec un dossier
spécifique pour les tests:
tests
├── e2e
│ ├── plugins
│ │ └── index.js
│ ├── specs
│ │ └── test.js
│ └── support
│ ├── commands.js
│ └── index.js
└── unit
└── example.spec.ts
Nous allons nous interesser à tests/unit.
Mais d'abord créons un petit composant pour afficher un compteur.
Modifions le fichier src/views/Home.vue
avec le contenu suivant:
<template>
<div class="home">
<h1 id="title">Counter value: {{ counter }}</h1>
<button id="increment" @click="increment">+ increment</button>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
@Component({
components: {},
})
export default class Home extends Vue {
counter = 0;
increment(): void {
this.counter++;
}
}
</script>
C'est un simple compteur qui affiche la valeur courante du compeur et qui contient deux boutons permettant d'incrémenter et décrementer le compteur.
Examples de test unitaires
Vérifier la valeur d'un texte
Maintenant créons un fichier Home.test.ts dans tests/unit: (you can remove the file: rm tests/unit/example.spec.ts)
// Importer les utilitaires de test
import { shallowMount } from "@vue/test-utils";
// Importer le composant que l'on souhaite tester
import Home from "@/views/Home.vue";
// Créer un groupe de test "Home.vue"
describe("Home.vue", () => {
// Créer un test dans le groupe
it("renders counter value", () => {
// Monter le composant et y accéder avec la variable wrapper
const wrapper = shallowMount(Home);
// Obtenir un élement du dom avec un sélecteur CSS
const title = wrapper.get("#title");
// Obtenir le text de l'élément
const text = title.text();
// Le comparer avec le texte attendu
expect(text).toMatch("Counter value: 0");
});
});
Le test vérifie la valeur par défaut du compteur. Les commentaires du codes permettent de comprendre chaque étape.
Pour lancer le test, vue à préparé des commandes pour nous: yarn test:unit
Jest se lance et on obtient le résultat suivant:
PASS tests/unit/Home.spec.ts
Home.vue
✓ renders counter value (16ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.138s
Ran all test suites.
Done in 2.92s.
Tester les évènements
Testons maintenant le clic des boutons increment:
// Créer un test pour les boutons
it.only("renders counter value after increment", async () => {
// Monter le composant et y accéder avec la variable wrapper
const wrapper = shallowMount(Home);
// Obtenir le bouton d'incrément
const increment = wrapper.get("#increment");
// Simuler le clic
await increment.trigger("click");
// Accéder à un élement spécifique avec un sélecteur CSS
const title = wrapper.get("#title");
// Obtenir le texte de l'élement
const text = title.text();
// Comparer le texte à la valeur attendue
expect(text).toMatch("Counter value: 1");
});
La commande await increment.trigger("click");
permet de simuler un click sur le boutton.
On remarque ici l'utilisateur du mot clé await
. Cela est du au caractère asynchrone des évènements.
Il faut aussi utiliser le mot clé async pour indiquer que la fonction est asynchrone:
it.only("renders counter value after increment", ...
Dans ce test j'ai ajouté .only
derrière le it
. Cela permet de ne lancer que ce test et d'ignorer les autres.
En lancant les tests on obtient le résultat suivant:
PASS tests/unit/Home.spec.ts
Home.vue
✓ renders counter value after increment (17ms)
○ skipped renders counter value
Test Suites: 1 passed, 1 total
Tests: 1 skipped, 1 passed, 2 total
Snapshots: 0 total
Time: 2.192s
Ran all test suites.
Done in 2.97s.
Travailler avec les fonctions asynchrones
Travailler avec des fonctions asynchrones peut être délicat. Il faut attendre que la promesse soit résolu avant de comparer les valeurs attendues. Voici un exemple pour illustrer le problème.
Ajoutons deux boutons avec un setTimeout
pour incrémenter 1s plus tard.
<template>
<div class="home">
<h1 id="title">Counter value: {{ counter }}</h1>
<button id="increment" @click="increment">+ increment</button>
<br />
<button id="increment-later" @click="incrementLater">
+ increment later
</button>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
@Component({
components: {},
})
export default class Home extends Vue {
counter = 0;
increment(): void {
this.counter++;
}
async incrementLater(): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => {
this.counter++;
resolve();
}, 1000);
});
}
}
</script>
Puis ajoutons un test pour ce bouton:
// Créer un test pour les boutons
it("renders counter value after later increment", async () => {
// Monter le composant et y accéder avec la variable wrapper
const wrapper = shallowMount(Home);
// Obtenir le bouton d'incrément
const increment = wrapper.get("#increment-later");
// Simuler le clic
await increment.trigger("click");
// Accéder à un élement spécifique avec un sélecteur CSS
const title = wrapper.get("#title");
// Obtenir le texte de l'élement
const text = title.text();
// Comparer le texte à la valeur attendue
expect(text).toMatch("Counter value: 1");
});
On remarque que le test ne passe pas yarn test:unit
.
FAIL tests/unit/Home.spec.ts
Home.vue
✕ renders counter value after increment (21ms)
○ skipped renders counter value
○ skipped renders counter value after increment
● Home.vue › renders counter value after increment
expect(received).toMatch(expected)
Expected substring: "Counter value: 1"
Received string: "Counter value: 0"
47 | const text = title.text();
48 | // Compare it to the expected text
> 49 | expect(text).toMatch("Counter value: 1");
| ^
50 | });
51 | });
52 |
at Object.<anonymous> (tests/unit/Home.spec.ts:49:18)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 2 skipped, 3 total
Snapshots: 0 total
Time: 2.245s
Ran all test suites.
En effet, le test opère avant que la promesse soit résolu. Donc le dom n'est pas encore mis à jour au moment de la vérification. Dans ce cas on peut utiliser des fonctions utilitaires pour attendre la résolution des promesses.
Dans notre cas, nous allons utiliser une fonction de jest qui permet de faire un mock sur les fonctions de temps comme setTimeout.
Ajouter la ligne suivante au début des tests:
jest.useFakeTimers();
Cela dit à Jest de remplacer les fonctions de temps pour nos tests.
Ensuite ajoutez les lignes suivantes avant de récupérer le texte du DOM:
// Demande à Jest d'executer tous les timers en attente
jest.runOnlyPendingTimers();
// Attend que Vue est mis à jour le DOM
await wrapper.vm.$nextTick();
Le code final est le suivant:
// Créer un test pour les boutons
it("renders counter value after later increment", async () => {
// Monter le composant et y accéder avec la variable wrapper
const wrapper = shallowMount(Home);
// Obtenir le bouton d'incrément
const increment = wrapper.get("#increment-later");
// Simuler le clic
await increment.trigger("click");
// Demande à Jest d'executer tous les timers en attente
jest.runOnlyPendingTimers();
// Attend que Vue est mis à jour le DOM
await wrapper.vm.$nextTick();
// Accéder à un élement spécifique avec un sélecteur CSS
const title = wrapper.get("#title");
// Obtenir le texte de l'élement
const text = title.text();
// Comparer le texte à la valeur attendue
expect(text).toMatch("Counter value: 1");
});
Utiliser le store Vuex
Le store peut utiliser des fonctions asynchrones (des actions). Dans ce contexte, il faut implémenter les tests de manière spécifique pour les tester correctements. On va donc 'mocker' le store:
Modifiez le composant Home.vue:
<template>
<div class="home">
<h1 id="title">Counter value: {{ counter }}</h1>
<button id="increment" @click="increment">+ increment</button>
<br />
<button id="increment-later" @click="incrementLater">
+ increment later
</button>
<button id="increment-store" @click="incrementStore">
+ increment store
</button>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
@Component({
components: {},
})
export default class Home extends Vue {
counter = 0;
increment(): void {
this.counter++;
}
async incrementLater(): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => {
this.counter++;
resolve();
}, 1000);
});
}
incrementStore(): void {
this.$store.dispatch("increment");
}
}
</script>
On y a ajouté un bouton increment store et une méthode pour appelé l'action increment du store.
Assuez-vous d'ajouter ces lignes en haut du fichier de test:
import Vuex from "vuex";
import { createLocalVue } from '@vue/test-utils'
const localVue = createLocalVue();
localVue.use(Vuex);
// Créer un test pour le store
it.only("renders counter value after increment", async () => {
// Create a mock store
// Créer un store mocké
const actions = {
increment: jest.fn(),
};
const store = new Vuex.Store({
actions,
});
// Monter le composant et y accéder avec le wrapper
const wrapper = shallowMount(Home, { store, localVue });
// Obtenir le bouton pour incrementer
const increment = wrapper.get("#increment-store");
// Simuler le clic
await increment.trigger("click");
expect(actions.increment).toHaveBeenCalled();
});
Ces lignes indiquent qu'il faut utiliser Vuex mais avec un objet local de vue plutôt que l'objet global. Dans ce test, ce n'est pas le résultat du clic qui est testé mais le fait que le clic appelle bien le store
Conclusion
Les différents cas d'utilisation suivants on été vue: vérification des textes du DOM, évènements, fonctions asynchrones, mocking du store Vuex.
Pour plus d'information vous pouvez vous référer à la documentation officiel.
- Vue Test Utils: https://vue-test-utils.vuejs.org/
- Jest: https://jestjs.io/