Faciliter les tests des promises quand nous utilions Mocha, Chai et Sinon

Hop, une petite extension à l'article "Sinon: restorer les espions et stubs automatiquements".


Ici, nous allons voir comment tester des Promises dans nos tests unitaires. Car nous savons que Mocha nous facilite déjà la tâche en nous proposant une mécanique bien spécifique de gestion de l'asynchronisme.

Par défaut, nous devons déclarer une variable dans notre "it" afin de pouvoir déclarer à Mocha que notre test est asynchrone, mais aussi de pouvoir dire quand cela se termine:


it('an unit test with a promise', function (done) {
   myService
      .myPromiseFunc(42)
      .then(function (response) {
         chai.expect(response).equals(21);
         done();
      })
      .catch(done);
});

it('an unit test with a promise in a fail case', function (done) {
   myService
      .myPromiseFunc(21)
      .then(function () {
         done('The then method shall not be called');
      })
      .catch(function (err) {
         chai.expect(err).to.be.instanceof(MyCustomError);
         done();
      })
      .catch(done);
});


Déjà, qu'est-ce que nous pouvons dire ? Nous sommes obligés de déclarer un "catch" sur nos tests de Promise, car si l'assertion n'est pas respecté, Mocha va lever une exception (du moins, dans le monde NodeJs). Du coup, par sécurité, nous devons déclarer cette exception.

Inversement, si nous espérons avoir une exception, nous devons déclarer un then au cas où l'exception n'est pas levé, quand même déclarer ce fameux catch ! Ce qui fait un peu de boilerplate de code.

Nous pouvons résumer ces deux précédents tests, en retournant la Promise dans le "it". Mocha saura que le test est asynchrone:

it('an unit test with a promise', function () {
   return myService
      .myPromiseFunc(42)
      .then(function (response) {
         chai.expect(response).equals(21);
      });
});

it('an unit test with a promise in a fail case', function () {
   return myService
      .myPromiseFunc(21)
      .then(function () {
         throw new Error('The then method shall not be called');
      })
      .catch(function (err) {
         chai.expect(err).to.be.instanceof(MyCustomError);
      });
});



Maintenant, imaginons, nous devons faire des "spies" et des "stubs" autour des Promises via Sinon, nous allons avoir des tests dans ce genre-là (dans un cas plutôt simple), ce qui ne donne pas une lecture toujours très claire du dit test:

const chai = require('chai');
const sinon = require('sinon');
 
describe('My awesome tests', function () {
   const MyService = require('../lib/my-service');
   let sandBox = null;
   
   beforeEach(function (){
        sandBox = sinon.sandbox.create();
    });
 
    afterEach(function () {
        sandBox.restore();
    });
   
   it('shall raise an exception if no parameter is set', function() {
      return MyService
         .findSomething()
         .then(function () {
            throw new Error('The then method shall not be called');
         })
         .catch(function(error) {
            chai.expect(error).to.be.exist;
            chai.expect(error).to.be.instanceof(Error);
            chai.expect(error.message).equals('You must set a parameter!');
         });
   });
   
   it('shall made an SQL query into the database', function() {
      let stubCall = sandBox.stub(MyService, 'queryInDatabase', function () {
         return Promise.resolve([{ 'value': 'some value' }]);
      });
   
      return MyService
         .findSomething(42)
         .then(function (results) {
            chai.expect(stubCall.called).to.be.true;
            chai.expect(stubCall.calledOnce).to.be.true;
            chai.expect(stubCall.calledWith(sinon.match.string, [42])).to.be.true;
            chai.expect(results).deep.equals(['some value']);
         });
   });
});



Ici, dans le dernier test, nous voyons que nous "mockons" un accès à la base de données afin de simuler la réponse, et que nous vérifions également que cette même fonction n'a bien été appelé une seule fois avec des paramètres spécifiques (notamment, en reprenant le paramètre fourni à la méthode que nous voulons tester originellement).

Pour faciliter la lecture (même si l'exemple précédent reste lisible. Cela devient plus compliqué quand nous devons en faire plusieurs), nous allons utiliser deux plugins:
Ces deux plugins vont étendre à la fois Chai  et Sinon pour étendre la syntaxe. Mais nous allons utiliser la notation "Mock" en association les "Expectations" de Sinon. Ce dernier permet de définir un contrat de test de manière plus "humaine" et fluide:



const chai = require('chai');
const sinon = require('sinon');
 
chai.use(require('chai-as-promised'));
require('sinon-as-promised');
 
describe('My awesome tests', function () {
   const MyService = require('../lib/my-service');
   let sandBox = null;
   
   beforeEach(function (){
        sandBox = sinon.sandbox.create();
    });
 
    afterEach(function () {
        sandBox.restore();
    });
   
   it('shall raise an exception if no parameter is set', function() {
      return MyService
         .findSomething()
         .then(function () {
            throw new Error('The then method shall not be called');
         })
         .catch(function(error) {
            chai.expect(error).to.be.exist;
            chai.expect(error).to.be.instanceof(Error);
            chai.expect(error.message).equals('You must set a parameter!');
         });
   });
   
   it('shall made an SQL query into the database', function() {      
      let stubCall = sandBox.mock(MyService);
      stubCall
         .expects('queryInDatabase')
         .once()
         .withArgs(sinon.match.string, [42])
         .resolves([{ 'value': 'some value' }]);
       
      return chai
          .expect(MyService.findSomething(42)).to.be.eventually.deep.equal(['some value'])
          .then(function () {
            findAllMock.verify();
          });
   });
});



Un peu plus sympa non ?


Commentaires

Posts les plus consultés de ce blog

ISO: liens & outils utiles

NodeJs et SSL: une petite analyse

Créer sa commande slack en quelques minutes