Dans ce guide, nous allons voir comment tester Vuex dans des composants grâce à vue-test-utils
.
Regardons un peu de code !
Ci-dessous, le composant que nous voulons tester. Il fait appel à des actions Vuex.
<template>
<div class="text-align-center">
<input type="text" @input="actionInputIfTrue" />
<button @click="actionClick()">Click</button>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default{
methods: {
...mapActions([
'actionClick'
]),
actionInputIfTrue: function actionInputIfTrue (event) {
const inputValue = event.target.value
if (inputValue === 'input') {
this.$store.dispatch('actionInput', { inputValue })
}
}
}
}
</script>
Pour les objectifs de ce test, on se fiche de ce que les actions font, ou à ce quoi le store ressemble. On doit juste savoir si ces actions sont lancées lorsqu'elles sont supposées l'être, et ce, avec les valeurs attendues.
Pour tester cela, on doit passer un store fictif à Vue lorsque l'on isole notre composant. TODO : Isoler, bon verbe ?
Au lieu de passer le store au constructeur de base de Vue, on peut le passer à - localVue. Une localVue est un constructeur à portée limitée de Vue sur lequel on peut effectuer des changements sans avoir à affecter le constructeur global.
Voyons à quoi cela ressemble :
import { shallow, createLocalVue } from 'vue-test-utils'
import Vuex from 'vuex'
import Actions from '../../../src/components/Actions'
const localVue = createLocalVue()
localVue.use(Vuex)
describe('Actions.vue', () => {
let actions
let store
beforeEach(() => {
actions = {
actionClick: jest.fn(),
actionInput: jest.fn()
}
store = new Vuex.Store({
state: {},
actions
})
})
it('appelle une action du store (actionInput) quand la valeur de l\'input est input et qu\'un évènement input est lancée', () => {
const wrapper = shallow(Actions, { store, localVue })
const input = wrapper.find('input')
input.element.value = 'input'
input.trigger('input')
expect(actions.actionInput).toHaveBeenCalled()
})
it('n\'appelle pas l\'action du store actionInput quand la valeur de l\'input n\'est pas input et qu\'un évènement input est lancée', () => {
const wrapper = shallow(Actions, { store, localVue })
const input = wrapper.find('input')
input.element.value = 'not input'
input.trigger('input')
expect(actions.actionInput).not.toHaveBeenCalled()
})
it('appelle l\'action du store actionClick quand le bouton est cliqué', () => {
const wrapper = shallow(Actions, { store, localVue })
wrapper.find('button').trigger('click')
expect(actions.actionClick).toHaveBeenCalled()
})
})
Que se passe-t-il ici ? Premièrement, on indique à Vue d'utiliser Vuex avec la méthode use
. C'est tout simplement une surcouche de Vue.use
.
On va ensuite créer un store fictif en appelant new Vuex.Store
avec nos propres valeurs. À noter que l'on indique uniquement nos actions, car on ne s'intéresse qu'à elles.
Les actions sont des fonctions de simulations de Jest. Ces fonctions nous donnent accès à des méthodes afin de réaliser des assertions si l'action a été appelée ou non.
On peut ensuite s'assurer dans nos tests que les actions ont été appelées au bon moment.
La manière dont on définit le store peut vous paraitre un peu étrange.
On utilise beforeEach
pour s'assurer que nous avons un store propre avant chaque test. beforeEach
est un hook de Mocha qui est appelé avant chaque test. Dans nos tests, on réassigne des valeurs aux variables du store. Si on ne le fait pas, les fonctions de simulations auraient besoin d'être automatiquement réinitialisées. Cela nous laisse la possibilité de changer le state dans nos tests, sans avoir à affecter les prochains.
La chose la plus importante à noter dans ce test est que l'on créer une simulation d'un store Vuex, qui est ensuite passé à vue-test-utils.
Génial, on peut désormais simuler des actions. Allons avoir comment simuler des getters !
<template>
<div>
<p v-if="inputValue">{{inputValue}}</p>
<p v-if="clicks">{{clicks}}</p>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default{
computed: mapGetters([
'clicks',
'inputValue'
])
}
</script>
C'est un composant relativement simple. Il affiche le résultat des getters clicks
et inputValue
. Encore une fois, on se fiche de savoir ce que ces getters retournent. On souhaite juste savoir si les résultats sont affichés correctement.
Jetons un œil à un test :
import { shallow, createLocalVue } from 'vue-test-utils'
import Vuex from 'vuex'
import Actions from '../../../src/components/Getters'
const localVue = createLocalVue()
localVue.use(Vuex)
describe('Getters.vue', () => {
let getters
let store
beforeEach(() => {
getters = {
clicks: () => 2,
inputValue: () => 'input'
}
store = new Vuex.Store({
getters
})
})
it('affiche state.inputValue dans le premier tag p', () => {
const wrapper = shallow(Actions, { store, localVue })
const p = wrapper.find('p')
expect(p.text()).toBe(getters.inputValue())
})
it('affiche stat.clicks dans le second tag p', () => {
const wrapper = shallow(Actions, { store, localVue })
const p = wrapper.findAll('p').at(1)
expect(p.text()).toBe(getters.clicks().toString())
})
})
Ce test est similaire à notre test sur les actions. On créer un store fictif avant chaque test, on le passe ensuite comme une option lorsque l'on appelle shallow
. Pour finir, on asserte que la valeur retournée par nos getters fictifs est bien affichée.
C'est génial, mais comment faisons-nous pour vérifier que nos getters retournent correctement les parties du state ?
Modules sont utiles pour séparer un store en plusieurs morceaux. Ils exportent des getters que l'on peut utiliser dans nos tests.
Jetons un œil à ce composant :
<template>
<div>
<button @click="moduleActionClick()">Click</button>
<p>{{moduleClicks}}</p>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
export default{
methods: {
...mapActions([
'moduleActionClick'
])
},
computed: mapGetters([
'moduleClicks'
])
}
</script>
Simple composant qui possède une action et un getter.
Et le test :
import { shallow, createLocalVue } from 'vue-test-utils'
import Vuex from 'vuex'
import Modules from '../../../src/components/Modules'
import module from '../../../src/store/module'
const localVue = createLocalVue()
localVue.use(Vuex)
describe('Modules.vue', () => {
let actions
let state
let store
beforeEach(() => {
state = {
module: {
clicks: 2
}
}
actions = {
moduleActionClick: jest.fn()
}
store = new Vuex.Store({
state,
actions,
getters: module.getters
})
})
it('appelle l\'action du store moduleActionClick quand le bouton est cliqué', () => {
const wrapper = shallow(Modules, { store, localVue })
const button = wrapper.find('button')
button.trigger('click')
expect(actions.moduleActionClick).toHaveBeenCalled()
})
it('affiche state.inputValue dans le premier tag p', () => {
const wrapper = shallow(Modules, { store, localVue })
const p = wrapper.find('p')
expect(p.text()).toBe(state.module.clicks.toString())
})
})