#CestFacile : Un peu de tests avec Enzyme

Allez, aujourd’hui, on se détend.

On va faire une petite pause dans la découverte de l’éco-système React et parler un peu philatélie-orgasmique.

Bon, j’ai bien réfléchi, et on va plutôt parler tests unitaires finalement.

Avant d’entrer dans le vif du sujet, autant parler TDD une bonne fois pour toutes.

J’adore le TDD

Test-Driven Development – Développement dirigé par les tests.

Contrairement à la croyance populaire, il ne s’agit pas vraiment de tester son application mais plutôt de la concevoir en écrivant des tests qui représentent le fonctionnement attendu, progressivement.

Les conséquences sont par contre effectivement que l’on dispose de tests que l’on peut run dans un pipe d’intégration continue automatiquement, pour éviter les régressions et aller facilement vers le déploiement continu, et potentiellement réduire le time-to-market.

En théorie, le TDD, c’est la vie, et on devrait tous s’y mettre. Dans la vraie vie, mettre en place du TDD dans un projet React, c’est – si ce n’est impossible, beaucoup trop chronophage avec un retour du investissement / temps dont je doute très fortement.

Exit le TDD dans mes projets React, donc, mais ça ne veut pas dire qu’on ne doit pas écrire de tests du tout. Et en fait dans un projet React, c’est même plutôt fun d’écrire des tests, à tel point que j’ai déjà écris quelques articles sur le sujet, notamment celui-ci, dont ce nouveau post est inspiré.

On reprend notre app

Si vous avez suivi mes précédents posts dans l’ordre, vous devriez avoir le même projet que moi. Sinon, vous pouvez reprendre tout ici :

https://github.com/GregoryBabonaux/react-c-est-facile

On va installer les dépendances dont on a besoin, l’avantage avec create-react-app, c’est qu’on aura pas trop de boilerplate à se taper.

Un petit coup de yarn pour commencer :

yarn add enzyme chai enzyme-adapter-react-16 react-test-renderer redux-test-utils

On va avoir besoin de mocker notre store pour pouvoir tester nos containers connectés à Redux

On a également  besoin d’un adapter pour utiliser Enzyme avec la dernière version de React (Fiber). Create-React-App reconnait immédiatement le fichier de setup suivant :

import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });

Dans ./src/setupTests.js

C’est à peu près tout, on va écrire notre premier test :

import React from 'react';
import ReactDOM from 'react-dom';
import {shallow, mount} from 'enzyme';
import {expect} from 'chai';
import {Provider} from 'react-redux'
import { createMockStore } from 'redux-test-utils';
import App from './App';
import initialState from '../../Store/initialState'

const store = createMockStore(initialState)

it('renders without crashing', () => {
 const wrapper = mount(<App store={store}/>)
});

it('renders a header', () => {
 const wrapper = shallow(<App store={store} />).dive()
 expect(wrapper.find('Header')).to.have.length(1)
})

Dans ./src/Scenes/App/App.test.js

Un peu d’explications :

  • On va utiliser shallow et mount, se référer à mon précédent article sur le sujet
  • On utilise redux-test-utils afin de… allez je suis sur que vous savez. Oui ! Pour mocker le store
  • On test ensuite deux choses :
    • La première, c’est de vérifier que le composant mount sans cracher
    • La seconde, c’est de tester la présence d’un header dans notre composant
  • Vous remarquerez que le premier test utilise mount (pour… monter le composant) tandis que le second utilise shallow. C’est parce qu’on veut tester le composant en isolation dans le second cas.

On lance le test dans console

yarn test

A ce stade, on a rien vu de nouveau par rapport à mon précédent article, les tests doivent être green comme dans le screen ci-dessous :

On va pas se leurrer, c’était la partie facile, on va maintenant aller un poil plus loin dans nos tests.

Tester Redux

On va commencer par tester nos actions  :

import * as actions from './actions'
import * as types from './actionTypes'
 
describe('auth actions', () => {
  it('should dispatch a login action with expectedAction', () => {
   const login = 'Bob'
   const password = 'toto123'
   const expectedAction = {
     type: types.LOGIN, 
     login, 
     password
   }
   expect(actions.login(login, password)).toEqual(expectedAction)
 })

 it('should dispatch a loggedIn action with expectedAction', () => {
   const expectedAction = {
     type: types.LOGGED_IN
   }  
   expect(actions.loggedIn()).toEqual(expectedAction)
 })
})

Dans ./src/Services/Auth/auth.test.js

Rien de bien sorcier au final, on importe nos actions et nos actionTypes, et on compare le résultat de nos dispatch.

Toujours green !

 

On doit maintenant tester notre reducer :

import Auth from './reducer'
import * as types from './actionTypes'

describe('auth reducer', () => {
  it('should return the initial state', () => {
    // On passe undefined pour le state car on a une valeur par défaut dans notre reducer
    expect(Auth(undefined, {})).toEqual({"loading": false, "logged": false})
  });

  it('should handle login action', () => {
    expect(Auth([], {
      type: types.LOGIN,
    })).toEqual({loading: true, logged: false})
  });

  it('should handle logged in action', () => {
    expect(Auth([], {
      type: types.LOGGED_IN,
    })).toEqual({loading: false, logged: true})
  })
})

Dans ./src/Services/Auth/reducer.test.js

Tellement facile qu’on pourrait faire ça en mangeant une pizza à l’ananas sur une planche de surf un jour de tsunami en écoutant du Michel Fugain.

Redux + React

Ca se complique un poil, on va maintenant tester nos composants et Redux ensemble.

Enfin… quand je dis que ça se complique, il s’agit ici de tester qu’on dispatch bien une action après avoir cliqué sur le submit d’un formulaire.

On trouve tout un tas d’articles à ce sujet, mais fondamentalement, tout ce dont on a besoin dans ce cas précis, c’est de vérifier que l’action est bien dispatch avec la méthode « isActionDispatched() » de redux-test-utils, après avoir simulé un submit :

import React from 'react';
import ReactDOM from 'react-dom';
import {shallow, mount} from 'enzyme';
import {expect} from 'chai';
import {Provider} from 'react-redux'
import { createMockStore } from 'redux-test-utils';
import Login from './Login';
import initialState from '../../Store/initialState'
const store = createMockStore(initialState)

describe('<Login /> container', () => {
 
 it('renders without crashing', () => {
   const wrapper = mount(<Login store={store}/>)
 });

 it('should not dispatch a login action when form in not submited', () => {
   const action = {
     type: 'LOGIN',
     login :'', 
     password: ''
   }
   const wrapper = mount(<Login store={store} />)
   expect(store.isActionDispatched(action)).to.be.false;
 })

 it('should dispatch a login action when form in submited', () => {
   const action = {
     type: 'LOGIN',
     login :'', 
     password: ''
   }
   const wrapper = mount(<Login store={store} />)
   const button = wrapper.find('Button')
   button.simulate('submit')
   expect(store.isActionDispatched(action)).to.be.true;
 })
})

Dans ./src/Scenes/Login/Login.test.js

On mount notre container Login, on trouve le bouton pour la soumission du form, on simule un click et on jette un oeil au store afin de savoir si l’action a bien été dispatch.

On teste aussi le fait que l’action ne doit pas être dispatch si le form n’est pas submit. On est d’accord, ça ne sert pas à grand chose, mais c’est pour que ce soit assez clair.

So ?

On a fait un petit tour en terme de couverture de tests.

On a bien entendu des cas qui peuvent devenir plus complexes. Par exemple, dans notre container Login, on a un conditional rendering basé sur la soumission de formulaire, qui affiche soit le formulaire, soit un spinner, et qu’on peut par exemple tester de cette façon :

it('should render a spinner if loading is true', () => {
 let newState = {...initialState, auth : {...initialState.auth, loading: true} }
 const newStore = createMockStore(newState)
 const wrapper = mount(<Login store={newStore} />)
 
 expect(wrapper.find('Form')).to.have.length(0)
 expect(wrapper.find('Loader')).to.have.length(1)
})

Et les sagas ?

Ah ah ! Vous aimez les fouets !

On va utiliser une nouvelle dépendance :

yarn add redux-saga-test-plan

Ici, on va simplement tester notre generator authenticate en modifiant un peu son fonctionnement (redux-saga-test-plan n’accepte pas de timeout de plus de 250ms).

Ce qu’on teste ici, c’est le fait que notre saga put bien une action loggedIn :

import { call, put } from 'redux-saga/effects';
import { expectSaga } from 'redux-saga-test-plan';
import {loggedIn} from './actions'

const someAPICalls = {
 tryLogin(payload){
   return new Promise( (resolve, reject) => {
     setTimeout(resolve, 200, 'faketokenforfun')
   })
 },
}

export function* authenticate(action){
 const token = yield call(someAPICalls.tryLogin, action.payload)
 yield put(loggedIn())
}

it('must put a loggedIn action', () => {
 const action = {
   type: 'LOGIN',
   payload: {
     login : '', 
     password: '',
   }
 }

return expectSaga(authenticate, action)
 .put(loggedIn())
 .run();
})

Dans ./src/Services/Auth/sagas.test.js

Youpi !

On verra un peu plus tard comment intégrer Cypress.io afin d’aller plus loin dans les tests.

Pour le repo git, c’est ici :

https://github.com/GregoryBabonaux/react-c-est-facile/tree/16b0516b1d73c83d97151355def40a92eb82eeb9

 

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *