Vue.js en TDD

le 19/05/2017 par Pierre Trollé
Tags: Software Engineering

Introduction :

On a tous entendu parler de Vue.js, le dernier framework JavaScript du moment. Certains l’ont déjà expérimenté. Mais au-delà du POC, il est temps de s’armer pour réaliser des grands projets Vue.js en production. Quoi de mieux pour cela que d’apprendre à utiliser Vue.js en TDD ? L’objectif de cet article est de partager les bases pour partir en TDD sur Vue.js. Afin d’y parvenir, on réalise un composant très simple, basé sur le template initialement généré par le Vue CLI, sur lequel on ajoute quelques fonctionnalités basiques du WebFront : deux boutons pour incrémenter unitairement ou aléatoirement (du nombre de points sur un dé) un compteur.

Pour plus d’informations concernant Vue.js, la documentation est très bien faite et se trouve ici : https://vuejs.org/v2/guide/. À consommer sans limite, en cas de problème.

Repo Git :

L’ensemble des commits associés à chaque étape de cet article est disponible sur ce repository GitHub, pratique quand on est perdu !

https://github.com/trollepierre/my_project_vue/commits/master

Pour commencer :

Comme la plupart des frameworks front, Vue.js est doté d'un bon CLI (CLI pour Interface en Ligne de Commande). Il contient l'essentiel des éléments à utiliser et on va suivre pas-à-pas son process :

Pour le lancer :

npm install --global vue-cli

Pour créer le nouveau projet, on utilise webpack :

vue init webpack le_nom_de_mon_projet

Parmi les options proposées, on recommande de garder les options de base, par défaut :

- le build au runtime + compiler

- vue-router

- eslint

- la configuration standard (la configuration AirBnb est déconseillée pour les débutants)

- Karma + Mocha, pour les tests

- Par contre, les tests e2e (end-to-end) ne seront pas évoqués dans cet article.

Et voilà, un nouveau projet, prêt à être lancé :

cd le_nom_de_mon_project

npm install

npm run dev

Et hop, l'environnement est déjà en place et une page est déjà prête sur  http://localhost:8080 :

Le commit correspondant : https://github.com/trollepierre/my_project_vue/commit/dd5d7c4

La Home Page générée par le Vue CLILa Home Page générée par le Vue CLI

Et voici l’arborescence déjà généré par le CLI :

Arborescence générée par le Vue CLIArborescence générée par le Vue CLI

On dispose déjà d'un test. C'est un test de template qui n'est pas très unitaire. En effet, il vérifie qu’après la construction de la vue, l’élément $el qui contient tout le HTML possède bien une balise h1 dont le texte est retourné par la variable {{ msg }}.

Hello.spec.js

import Vue from 'vue' import Hello from '@/components/Hello'

describe('Hello.vue', () => { it('should render correct contents', () => { const Constructor = Vue.extend(Hello) const vm = new Constructor().$mount() expect(vm.$el.querySelector('.hello h1').textContent) .to.equal('Welcome to Your Vue.js App') }) })

0) Environnement de travail

Quelque soit l’IDE, il est recommandé d’utiliser les plugins Vue.js. Ils sont déjà bien opérationnels et fournissent une bonne aide au développement. Pour réaliser cet article, c’est IntelliJ avec le plugin Vue.js qui a été utilisé.

Avant propos

Afin de simplifier l’écriture des tests, je recommande de rendre globale la variable vm (pour Vue mounted : vm représente l'instance Vue après la création et avec le template chargé : voir le Diagramme de cycle de vie) et d’extraire un beforeEach à insérer avant les assertions. On pourra ainsi accéder à la “vue montée” vm, déjà construite dans chaque assertion.

Hello.spec.js

let vm

beforeEach(function () { const Constructor = Vue.extend(Hello) vm = new Constructor().$mount() })

1)  Tester les data

Dans un premier temps, on va employer une approche plus micro et tester unitairement la valeur de la variable msg. Pour cela, on utilise l’accesseur vm.$data.nom_de_variable.

Hello.spec.js

it('should check that msg is Welcome to Your Vue.js App', () => { expect(vm.$data.msg).to.equal('Welcome to Your Vue.js App') })

Ce test pointe directement sur la variable. Cela permet de ne pas devoir modifier deux tests afin de modifier l’initialisation de la variable msg.

Hello.spec.js

it('should render my msg inside a h1', () => { expect(vm.$el.querySelector('.hello h1').textContent) .to.equal(vm.$data.msg) })

Une autre solution est de fournir la variable directement au constructeur du test.

Hello.spec.js

it('should render correct contents', () => { const data = { data: { msg: 'plop' } } const Constructor = Vue.extend(Hello) vm = new Constructor(data).$mount() expect(vm.$el.querySelector('.hello h1').textContent) .to.equal('plop') })

Le commit correspondant : https://github.com/trollepierre/my_project_vue/commit/bb5fa77

On peut désormais ajouter facilement de nouvelles variables, comme un compteur dont la valeur doit être égale à 0 par défaut :

Hello.spec.js

it('should create a counter with zero value', () => { expect(vm.$data.counter).to.equal(0) })

Pour faire passer ce test le plus simplement possible, on peut ajouter un compteur parmi nos data :

Hello.vue

data () { return { msg: 'Welcome to Your Vue.js App', counter: 0 } }

Le commit correspondant : https://github.com/trollepierre/my_project_vue/commit/6d46beb

On souhaite ajouter le compteur au template :

⇒ Le test associé devra vérifier que la valeur counter correspond bien au contenu textuel d’un div avec une class counter :

Hello.spec.js

it('should render counter with counter data value', () => { // Given const data = { data: { counter: 48 } } const Constructor = Vue.extend(Hello)

// When vm = new Constructor(data).$mount()

// Then expect(vm.$el.querySelector('.hello div.counter').textContent) .to.equal('48') })

⇒ On lance les tests et on vérifie qu’il est bien rouge pour la bonne raison.

⇒ On écrit le code le plus simple pour faire passer notre test.

Hello.vue

<div class="counter">{{ counter }}</div>

⇒ On lance les tests, c’est bien vert !

⇒ On se demande si on peut refactorer quelque chose

⇒ On peut se consacrer à notre prochaine assertion

Le commit correspondant : https://github.com/trollepierre/my_project_vue/commit/a7b267a

2) Tester le name

Pour tester le name de notre Vue, il suffit d’utiliser l’accesseur $options.

$options renvoie tous les options nécessaires à l'instanciation de la vue. Comme par exemple, le nom de la vue, ses méthodes, ses directives ou ses composants.

Hello.spec.js

it('should check the name of my vue', () => { expect(vm.$options.name).to.equal('hello') })

Le commit correspondant : https://github.com/trollepierre/my_project_vue/commit/6c80506

3) Créer un composant

  1. a) Poser un test sur le name

On souhaite maintenant ajouter un composant : une sorte de bouton spécial sur lequel l’utilisateur doit cliquer : ClickMeButton. D’abord on créé une classe de test associée ClickMeButton.spec.js (sur le même modèle que Hello.spec.js) et on teste le name :

ClickMeButton.spec.js

import Vue from 'vue' import ClickMeButton from '@/components/ClickMeButton'

describe('ClickMeButton.vue', () => { let vm

beforeEach(function () { const Constructor = Vue.extend(ClickMeButton) vm = new Constructor().$mount() })

it('should check the name of my vue', () => { expect(vm.$options.name).to.equal('clickMeButton') }) })

Maintenant on peut ajouter le composant :

ClickMeButton.vue

<template> <div class="clickMeButton"></div> </template>

<script> export default { name: 'clickMeButton' } </script>

Le commit correspondant : https://github.com/trollepierre/my_project_vue/commit/c774799

  1. b) Poser un test sur des éléments du template

On souhaite ajouter un bouton dans le template :

ClickMeButton.spec.js

it('should render button with text Click Me Button', () => { expect(vm.$el.querySelector('.clickMeButton button').textContent) .to.equal('Click Me Button') })

Le code à insérer dans le ClickMeButton.vue est donc tout simple :

ClickMeButton.vue

<template> <div class="clickMeButton"> <button>Click Me Button</button> </div> </template>

<script> export default { name: 'clickMeButton' } </script>

Le commit correspondant : https://github.com/trollepierre/my_project_vue/commit/94c6a11

  1. c) Poser un test sur les props du composant

Chaque composant Vue.js a son propre scope.  Cela signifie qu’on ne peut pas utiliser les données d’un composant parent depuis un composant enfant. C’est pour cela que les composants parents appellent les composants enfants en leur passant des `props` (un raccourci pour “properties”).

On souhaite pouvoir éditer le texte affiché par le bouton pour chaque utilisation du composant ClickMeButton. Pour cela, on utilise une props :

Dans le test, on modifie le beforeEach pour passer une propsData à la création du composant.

ClickMeButton.spec.js

beforeEach(function () { // Given const config = { propsData: { message: 'Click Me Button' } }

// When const Constructor = Vue.extend(ClickMeButton)

// Then vm = new Constructor(config).$mount() })

Ainsi on peut modifier le fichier ClickMeButton.vue et retirer toute mention de la chaîne de caractères 'Click Me Button'

ClickMeButton.vue

<template> <div class="clickMeButton"> <button>{{ message }}</button> </div> </template>

<script> export default { name: 'clickMeButton', props: ['message'] } </script>

Le commit correspondant : https://github.com/trollepierre/my_project_vue/commit/c1731f7

4) Tester la présence d’un composant enfant

Dans un premier temps, on importe l’élément ClickMeButton dans la classe de spec :

Hello.spec.js

import ClickMeButton from '@/components/ClickMeButton'

Puis on teste qu’il est bien présent à l’instantiation de la classe :

Hello.spec.js

it('should include a clickMeButton', () => { const clickMeButton = vm.$options.components.ClickMeButton expect(clickMeButton).to.contain(ClickMeButton) })

On peut faire évoluer le code en ajoutant l’import du composant dans le script. Ensuite, en ajoutant également le nom du composant parmi les enfants à exporter dans le template. Enfin, on ajoute le nom du composant en kebab-case dans notre template (Vue transforme le nom du composant parmi ses composants de PascalCase en kebab-case)

Hello.vue

<template>  <div class="hello">      <click-me-button></click-me-button>  </div> </template>

<script> import ClickMeButton from '../components/ClickMeButton' export default {  name: 'hello',  components: {    ClickMeButton  } } </script>

Le commit correspondant : https://github.com/trollepierre/my_project_vue/commit/2789185

5)    Tester les props avec lesquels on appelle un composant

On souhaite modifier le message renvoyé par le clickMeButton. Pour ce faire, c’est  Hello.vue qui doit définir la propriété message à chaque utilisation d’un composant ClickMeButton. Pour cela, on utilise les props. Voici comment vérifier que le composant ClickMeButton a bien été appelé avec la prop message :

Hello.spec.js

it('should define a message to put inside the clickMeButton', () => { expect(vm.$options.components.ClickMeButton.props).to.haveOwnProperty('message') })

Pour faire passer ce test au vert, il suffit d’ajouter la propsData message au bouton click-me-button présent dans le template.

Hello.vue

<click-me-button message="Increment counter"></click-me-button>

Et pour vérifier le texte contenu dans le bouton, passé par la variable message :

Hello.spec.js

it('should verify textContent of the Click Me Button', () => {  expect(vm.$el.querySelector('.clickMeButton button').textContent) .to.equal('Increment counter') })

Le commit correspondant : https://github.com/trollepierre/my_project_vue/commit/1c20a6f

6) Tester une méthode

Il est très facile d’accéder aux méthodes pour les tester. Il suffit d’effectuer l’appel de la méthode sur l’objet vm que l’on manipule dans les tests.

Hello.spec.js

describe('incrementCounter', function () { it('should increment the counter to 1', () => {    // When    vm.incrementCounter()    // Then expect(vm.$data.counter).to.equal(1) }) })

On peut désormais créer la méthode :

Hello.vue

methods: { incrementCounter: function () { this.counter += 1 } }

Le commit correspondant : https://github.com/trollepierre/my_project_vue/commit/4f2136d

Pas trop de difficultés pour tester des méthodes, tant mieux c’est ce qu’on utilise le plus fréquemment !

On souhaite également ajouter au composant ClickMeButton une nouvelle méthode onButtonClick qui émet un événement ‘buttonHasBeenClicked’. L’action à réaliser est simple : on applique la nouvelle méthode à la vue une fois montée : vm.onButtonClick()

Le test n’est qu’un spy avec Sinon.js

ClickMeButton.spec.js

describe('onButtonClick', function () { it('should emit click ', () => {    // Given    sinon.spy(vm, '$emit')      // When    vm.onButtonClick()

// Then   expect(vm.$emit).to.have.been.calledWith('buttonHasBeenClicked') }) })

On peut implémenter la méthode :

ClickMeButton.vue

<script> export default {   name: 'clickMeButton',  props: ['message'],  methods: { onButtonClick: function () {   this.$emit('buttonHasBeenClicked') }   } } </script>

Le commit correspondant : https://github.com/trollepierre/my_project_vue/commit/31ed02d

7)    Binder la méthode au template :

La méthode est bien bien testée mais elle n’est toujours pas intégrée au site. Pour cela, on va l’ajouter au template et l’affecter à un bouton.

On souhaite vérifier que la méthode ait bien été appelée.

ClickMeButton.spec.js

it('should emit an event when button is clicked', () => { // given sinon.spy(vm, '$emit') const button = vm.$el.querySelector('button')

// when button.click()

// then   expect(vm.$emit).to.have.been.calledWith('buttonHasBeenClicked') })

On peut ainsi ajouter le bout de code associé :

ClickMeButton.vue

<template> <div class="clickMeButton"> <button v-on:click="onButtonClick">{{ message }}</button> </div> </template>

Le commit correspondant : https://github.com/trollepierre/my_project_vue/commit/a12fa30

8) Tester la transmission d’événements d’enfant au parent

L’objectif est désormais d’incrémenter un compteur à chaque click sur le bouton. Une façon de faire est de brancher l’appel à la méthode incrementCounter à chaque fois que le composant ClickMeButton émet l’événement « buttonHasBeenClicked »

Actuellement je n’ai pas trouvé comment stubber l’émission d’événements renvoyés par l’enfant . Alors on passe par un TI, simulant le click sur le bouton précédemment créé, et on vérifie que la variable counter a bien été incrémentée.

Hello.spec.js

it('should increment counter when button from ClickMeButton is clicked', () => { // given let button = vm.$el.querySelector('.clickMeButton button')

// when button.click()

// then expect(vm.$data.counter).to.equal(1) })

Et le code associé, traversé par le test.

Hello.vue

<click-me-button message="Increment counter"

v-on:buttonHasBeenClicked="incrementCounter"></click-me-button>

Le commit correspondant : https://github.com/trollepierre/my_project_vue/commit/a3c89ac

Désormais on peut afficher le résultat via la commande :

npm run dev

Et le résultat :

Animation du clic sur le bouton de la pageA chaque clic sur le bouton ClickMeButton, le compteur affiché à l'écran est bien incrémenté.

9) Faire appel à une API externe : l’utilisation de Vue-resource http

Pour faire appel à une API externe, on a besoin de vue-resource :

npm install vue-resource  - - save

Pour utiliser VueResource dans chaque objet Vue du code de production, on l’insère au niveau du fichier src/main.js ainsi que dans le fichier Hello.spec.js

src/main.js et Hello.spec.js

import VueResource from 'vue-resource' Vue.use(VueResource)

Pour réaliser les prochains tests, on aura besoin de stubs et de promesses, c’est pourquoi on utilisera sinon-stub-promise et son intégration dans karma  karma-sinon-stub-promise

npm install sinon-stub-promise - - save-dev

npm install karma-sinon-stub-promise - - save-dev

Penser à ajouter sinon-stub-promise dans le fichier de configuration de karma :

test/unit/karma.conf.js

frameworks: ['mocha', 'sinon-stub-promise','sinon-chai', 'phantomjs-shim']

L’API rollthedice renvoie, à chaque appel, le résultat d’un dé à 6 faces. On utilisera cette URL : http://setgetgo.com/rollthedice/get.php

Il est temps d’écrire le prochain test. Dans un premier temps, il faut stubber l’appel d’une URL (Vue.http.get) grâce à une promesse Sinon.js. Il est primordial de réaliser le stub avant la construction de l’objet. Ensuite vient une nouvelle construction de la “vue montée” et l’appel de la méthode à tester qu’on appellera incrementFromTheDice.

Les assertions portent sur l’URL appelées par l’API.

A la fin du test, il ne faut surtout pas oublier de restaurer l’état via la méthode Vue.http.get.restore. En absence de restauration, cela engendre des conflits avec les autres tests de promesses, et c’est le mal !

Hello.spec.js

describe('incrementFromTheDice()', () => { it('should call api to get the dice number', () => {    // given    sinon.stub(Vue.http, 'get').returnsPromise()

// construct vue    const Constructor = Vue.extend(Hello)    const vm = new Constructor().$mount()

// when    vm.incrementFromTheDice()

// then    expect(Vue.http.get).to.have.been.calledWith('http://setgetgo.com/rollthedice/get.php')

// after    Vue.http.get.restore() }) })

Le code de production associé :

Hello.vue

incrementFromTheDice: function () { this.$http.get('http://setgetgo.com/rollthedice/get.php') }

Le commit correspondant : https://github.com/trollepierre/my_project_vue/commit/08b4bcd

On peut ajouter le résultat du dé à la variable affichée par le compteur, en renvoyant un résultat via la promesse :

Hello.spec.js

it('should call increment counter from API answer', () => { // given const promiseCall = sinon.stub(Vue.http, 'get').returnsPromise() promiseCall.resolves({ body: '5' })

// construct vue const Constructor = Vue.extend(Hello) const vm = new Constructor({ data: { counter: 6 } }).$mount()

// when vm.incrementFromTheDice()

// then expect(vm.$data.counter).to.equal(11)

// after Vue.http.get.restore() })

La fonction parseInt() est nécessaire pour convertir la chaîne de caractères renvoyée par l’API en entier. Et on ajoute la valeur obtenue au compteur :

Hello.vue

incrementFromTheDice: function () { this.$http.get('http://setgetgo.com/rollthedice/get.php')    .then((response) => {      this.counter += parseInt(response.body, 10)    }) }

Le commit correspondant : https://github.com/trollepierre/my_project_vue/commit/dac8dd2

Il faut également tester le cas où l’API renvoie une erreur. Dans ce cas, on peut décider de réinitialiser le compteur à 0

Hello.spec.js

it('should reinit counter when api rejects error', () => { // given const promiseCall = sinon.stub(Vue.http, 'get').returnsPromise() promiseCall.rejects()

// construct vue const Constructor = Vue.extend(Hello) const vm = new Constructor({ data: { counter: 6 } }).$mount()

// when vm.incrementFromTheDice()

// then expect(vm.$data.counter).to.equal(0)

// after Vue.http.get.restore() })

Le code de production devient donc :

Hello.vue

incrementFromTheDice: function () { this.$http.get('http://setgetgo.com/rollthedice/get.php')    .then((response) => {      this.counter += parseInt(response.body)    }, () => {      console.log('La Base semble être KO !')      this.counter = 0    }) }

Le commit correspondant : https://github.com/trollepierre/my_project_vue/commit/fd38826

Et enfin, pour visualiser les résultats, il ne reste que l’intégration du bouton sur la page :

Hello.spec.js

it('should incrementFromTheDice when button roll-the-dice is clicked', () => { // given let button = vm.$el.querySelector('button.roll-the-dice') const promiseCall = sinon.stub(Vue.http, 'get').returnsPromise() promiseCall.resolves({ body: '5' })

// when button.click()

// then expect(vm.$data.counter).to.equal(5)

// after Vue.http.get.restore() })

Et on ajoute un bouton pour lancer le dé :

Hello.vue

<button class="roll-the-dice" v-on:click="incrementFromTheDice">ROLL THE DICE</button>

Le commit correspondant : https://github.com/trollepierre/my_project_vue/commit/b5912ad

10) Conclusion

Et voilà une page assez simple à réaliser ! Elle contient deux boutons pour incrémenter unitairement ou aléatoirement un compteur, c’est très basique évidemment. Mais l’intérêt de cet article est la réalisation d’un code qui suit totalement les principes de TDD et totalement couvert par des tests qui sont les plus fins, les plus proches de la réalité.

Dans cet article, on a pris le temps de détailler les principaux tests réalisables avec le nouveau Framework Vue.js. Cet article voulait montrer qu’il est possible de suivre la méthodologie TDD avec Vue.js et que les tests étaient à la portée de tous.

Dans un prochain article, on pourra détailler comment tester les méthodes created() à la création de la Vue, ou les directives.