Il y a 3 années · 7 minutes · Back

Développer et tester un client http en Node.js

L’appel à un service http externe est un cas de figure très courant lorsqu’on développe un applicatif en Node.js. En production, un tel service peut être instable et il est primordial que cette instabilité ne mette pas en danger votre applicatif. Quelque soit le projet ou le service externe, les différents cas à envisager sont toujours les mêmes.

En partant d’un cas concret et simple, nous verrons dans cet article quelles sont les différentes étapes à suivre pour développer et tester un client HTTP robuste en Node.js.

Notre exemple

Notre objectif est de créer une méthode countEmployees qui renvoie le résultat de la requête HTTP suivante :

GET http://localhost:3000/employees/count

{"count":1200}

Etape 1 : le cas nominal

Dans cette étape, nous allons écrire une première implémentation de notre client HTTP et tester le cas où le service externe répond correctement. Pour l’écriture des tests nous utiliserons mocha.

Pour commencer, écrivons un test qui vérifie que la valeur retournée par notre client est la valeur renvoyée par le service externe :

describe('employees count service', function() {
  it('should return 1200', function() {

    client.countEmployees().then(function(count) {
      count.should.equal(1200);
    }).done();
  });
});

Ensuite, écrivons une implémentation de notre méthode countEmployees :

var Q = require('Q'),
  request = require('request'),
  config = require('./config');

exports.countEmployees = function() {
  var deferred = Q.defer();

  var options = {
    url: config.employeeCountUrl,
    json: true
  };

  request(options, function (error, response, body) {
    var employeesCount = body.count;
    deferred.resolve(employeesCount);
  });

  return deferred.promise;
};

A noter que dans cette implémentation nous utilisons deux modules : request qui simplifie grandement les requêtes HTTP en Node.js et Q pour les promesses.

Ici, notre test entraîne un véritable accès HTTP vers notre service externe. Pour que le test passe, notre service HTTP doit donc être allumé et renvoyer toujours la même valeur. Il est donc essentiel de trouver un moyen de simuler le service. Pour cela, nous allons utiliser le module nock qui permet de mocker les accès HTTP dans Node.js.

La prise en main de nock est très facile grâce à son recorder. Tout d’abord, rajoutons l’instruction suivante dans notre test :

beforeEach(function() {
  nock.recorder.rec();
});

A l’exécution du test, on aperçoit les lignes suivantes dans la console :

<<<<<<-- cut here -->>>>>>
nock('http://localhost:3000')
  .get('/employees/count')
  .reply(200, {"count":1200}, { 'x-powered-by': 'Express', 'content-type': 'application/json; charset=utf-8', 'content-length': '14', etag: 'W/"e-2043423703"', date: 'Wed, 09 Jul 2014 20:59:20 GMT',connection: 'keep-alive' });
<<<<<<-- cut here -->>>>>>

Il s’agit simplement de l’instruction à copier-coller dans le test pour simuler l’exécution de la requête http://localhost:3000/employees/count. N’hésitez pas à nettoyer ce qui ne vous intéresse pas dans l’instruction, comme par exemple les champs du header de la réponse. 

Une autre instruction nock qui est intéressante est celle-ci : nock.disableNetConnect(). Elle permet d’interdire tous les accès HTTP. En combinant ces deux instructions nock, notre test devient : 

describe('employees count service', function() {
  beforeEach(function() {
    nock.disableNetConnect();
  });

  it('should return employees count', function() {
    nock('http://localhost:3000')
      .get('/employees/count')
      .reply(200, {count:1986});

    client.countEmployees().then(function(count) {
      count.should.equal(1986);
    }).done();
  });
});

Désormais, lorsque le client exécute sa requête, ce n’est pas le service externe qui répond, mais nock. De plus avec l’instruction disableNetConnect, on a la garantie qu’aucune requête HTTP supplémentaire n’est effectuée. Eteignons le service HTTP externe et relançons notre test… il passe !

Notre premier objectif est atteint : nous avons une première implémentation de notre client ainsi qu’un test unitaire qui couvre le cas nominal.

Etape 2 : le service est en erreur

Avec l’utilisation de nock dans notre test, il est maintenant beaucoup plus facile de tester nos cas d’erreur. Dans cette deuxième étape, nous voulons gérer le cas où notre service renvoie une erreur HTTP, par exemple une erreur 500. Etes-vous sûr que votre applicatif sait gérer ce cas ?

Ajoutons un nouveau test et simulons le fait que notre service renvoie une 500 :

it('should return an error if http service is in error', function(done) {
  nock('http://localhost:3000')
    .get('/employees/count')
    .reply(500, {});

  client.countEmployees().then(function(count) {
    done(new Error('method should return an error'));
  }).catch(function() {
    done();
  });
});

Notre test est en erreur. En effet, notre client ne renvoie pas d’erreur si le service renvoie une 500, mais un résultat null. Pas de panique, nous pouvons facilement corriger notre implémentation en renvoyant une erreur si le statut HTTP de la réponse n’est pas dans les 200 :

if (response.statusCode >= 300) {
  deferred.reject(new Error('Service has an invalid status code : ' + response.statusCode));
}

L’objectif de l’étape 2 est atteint : notre implémentation sait gérer les erreurs HTTP.

Etape 3 : le service ne renvoie pas les données attendues

Dans cette troisième étape, nous voulons vérifier que notre implémentation réagit correctement si notre service HTTP ne renvoie pas les données attendues.

Simulons donc dans un troisième test, le fait que notre service ne renvoie pas de champ count mais un autre champ. Avec nock, encore une fois, c’est facile :  

whenEmployeesCountIsCalled().reply(200, {nb:1986});

A noter qu’ici on a factorisé dans notre test l’appel à nock grâce à la méthode suivante :

var whenEmployeesCountIsCalled = function() {
    return nock('http://localhost:3000')
      .get('/employees/count');
  };

Notre test échoue, ce qui prouve que notre implémentation initiale ne sait pas gérer ce cas. Pour corriger ce problème, il suffit de vérifier si le champ attendu est bien présent dans la réponse HTTP :

if (!employeesCount) {
  deferred.reject(new Error('Service did not return employees count'));
}

L’objectif de notre étape 3 est atteint : nous savons gérer le cas où notre service ne renvoie pas les données attendues.

Etape 4 : le service ne répond pas dans les temps

Dans cette quatrième étape, nous voulons tester le cas où le temps de réponse de notre service externe est trop long. Si le service est instable et répond très lentement, il est important que notre applicatif n’attende pas trop longtemps une réponse. 

Voyons comment nock nous permet de tester ce cas :

whenEmployeesCountIsCalled()
  .delayConnection(1500)
  .reply(200, {count:1986});

Ici, l’instruction delayConnection permet de retarder la réponse HTTP de 1500 ms.

Dans le cas où le service externe est trop lent, par exemple si nous n’avons pas de réponse après 500 ms, nous souhaitons que notre client interrompe la connexion et renvoie une erreur. 

Avec le module request, il est facile de préciser un timeout :

var options = {
  url: config.employeeCountUrl,
  json: true,
  timeout: 500
};

Puis il suffit de tester la valeur de l’objet error :

if (error) {
  deferred.reject(error);
  return;
}

Notre test passe désormais. L’étape 4 est terminée : notre client est protégé des accès trop lents grâce à un timeout.

Etape 5 : le service n’est pas connu

Dans cette dernière étape, nous voulons vérifier le comportement de notre client dans le cas où le service n’est pas connu. Pour cela nock ne peut pas nous aider. Par contre, il suffit de faire une vraie requête HTTP sur un host qui n’existe pas. Ajoutons donc un nouveau test :

it('should return an error if service is unavailable', function(done) {
  client = rewire('../client/employees.client');
  client.__set__('config', {
    configEmployeeCountUrl: 'http://doesnotexist.localhost:3000/employees/count'
  });

  client.countEmployees().then(function() {
    done(new Error('method should return an error'));
  }).catch(function() {
    done();
  });
 });

A noter qu’on utilise le module rewire pour surcharger l’object config utilisé par notre client, et donc préciser une url qui ne répondra pas.

Le test passe du premier coup. En effet, la correction apportée lors de l’étape 4 permet également d’intercepter ce type d’erreur.

Objectif atteint ! Nous avons terminé l'implémentation de notre client HTTP !

Conclusion

Un service en erreur, cassé, lent ou éteint : les aléas de la production sont nombreux et il est important que votre applicatif reste stable dans ces différents cas. Nous avons vu qu’avec quelques tests et grâce à nock, il est possible de reproduire chacune de ces erreurs et de construire un code robuste.

Nous n’avons couvert que quelques fonctionnalités de nock. N’hésitez pas à creuser un peu plus la documentation qui est très bien faite : https://github.com/pgte/nock.

Pour finir, vous pourrez trouver le code complet de l’article et les différentes modifications successives apportées sur le repository github suivant : https://github.com/jsebfranck/node-http-example.

Laisser un commentaire

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