I. Préparation

II. Mocks

Dans le tutoriel précédent, j'ai décrit les tests d'une implémentation personnalisée de Backbone.sync à l'aide des espions de Sinon.JS. Cela fonctionne bien dans notre situation où la couche de transport n'est pas nécessairement fixée. Sinon.JS inclut Fake XMLHttpRequest, mais à ce que je sais, cela ne fonctionnera pas avec l'API de Google. Cette semaine, je veux présenter un autre concept de tests que Sinon.JS fournit : les mocks.

Les mocks sont de fausses méthodes permettant d'être enregistrées. Historiquement, vous trouverez les mocks utilisés dans les tests unitaires où des I/O se produisent. Si vous testez la logique métier, vous n'avez pas besoin de vérifier si un fichier a été écrit ou un appel réseau a été effectué, il est souvent préférable de fixer une attente pour s'assurer que l'API appropriée a été appelée.

Dans Sinon.JS, la création d'un mock retourne un objet qui peut être initialisé avec ce que l'on désire. L'API est chainable. Ce que vous visez à faire est « dès que cette méthode est appelée, s'assurer qu'elle l'a été avec ces paramètres ». Cela peut être fait par le biais de mocks en mettant en place ce à quoi on s'attend à l'aide de matchers.

Les matchersMatchers sont similaires à des exigences. Ils peuvent être utilisés pour vérifier que les arguments sont de types primitifs aux instances d'un constructeur ou sont des valeurs littérales.

Dernièrement, nous avons utilisé les espions pour s'assurer que l'API de Google est accessible de la manière attendue. Les mocks pourraient servir à cela également. Nous ne nous soucions pas vraiment de la requête tant qu'une opération particulière dîtes CRUD est demandée. La signature de Backbone.gapiRequest est request, method, model, options. L'argument method est généralement ce qui nous intéresse. Par conséquent, pour mettre en place le fait que la sauvegarde d'une tâche existante provoque une update, nous pouvons utiliser un mock avec sinon.match.object :

 
Sélectionnez
var mock = sinon.mock(Backbone);
mock.expects('gapiRequest').once().withArgs(sinon.match.object, 'update');
 
// Faire des manipulations UI pour que la tâche soit éditée et le formulaire envoyé
mock.verify();

III. Mocks comparés aux Spies

L'exemple précédent ressemble un peu à celui des espions de l'article précédent. D'ailleurs, utiliser les espions pour faire la même chose nécessite moins de code. Alors, quand doit-on utiliser les mocks et quand doit-on utiliser les spies (les espions) ? Les mocks vous donnent un contrôle précis de l'ordre et du comportement des appels des méthodes. Les spies ont une API différente qui met l'accent sur la vérification d'utilisation des méthodes ou des callback. Si vous testez une méthode qui accepte un callback, vous pouvez passer par un espion pour voir comment le callback est utilisé. Avec un mock, le callback se fera par le système durant le test et vous pouvez configurer ce que vous désirez à ce sujet.

Quand il s'agit de tests UI (interface utilisateur) - déclencher des actions de l'interface pour déclencher le code - je trouve que c'est plus facile de traiter toute la pile de Backbone dans son ensemble et d'utiliser des espions pour s'assurer que le comportement attendu se produise. Plutôt que d'écrire un test pour chaque modèle, vue et collection, il est plus logique de manipuler l'interface utilisateur et de relier les opérations concernant le modèle ou la synchronisation pour vérifier le résultat.

Lors des tests précédents sur les listes, je n'aurai probablement pas pu utiliser les mocks parce que ceux-ci doivent avoir une relation plus étroite avec une méthode donnée lors du test. Les types de tests que nous écrivons impliquent plus d'une méthode, donc des espions sur le DOM sont plus judicieux.

IV. Exemple de mocks

Un bon endroit pour utiliser des mocks est lors des tests dans le fichier app/js/gapi.js. Disons que nous sommes intéressés par le fait de s'assurer que gapiRequest est appelée par Backbone.sync. Nous pourrions utiliser des mocks :

 
Sélectionnez
test('gapiRequest est appelée par Backbone.sync', function() {
  var mock = sinon.mock(Backbone);
  mock.expects('gapiRequest').once();
  Backbone.sync('update', model, {});
  mock.verify();
});

Cela force Backbone.sync à appeler gapiRequest une fois. Ce test ne vérifie pas le comportement de gapiRequest elle-même, juste le fait qu'elle est appelée.

Une chose étrange de la version personnalisée de l'API de Backbone.sync est que Task.prototype.get est appelée deux fois : une fois pour récupérer l'ID de la tâche et une autre pour obtenir l'ID de la liste. Nous pourrions le tester avec des mocks si on l'a jugé important :

 
Sélectionnez
test('Ensure Task.prototype.get est appelé deux fois', function() {
  var mock = sinon.mock(model);
 
  mock.expects('get').twice().returns(model.id);
  Backbone.sync('update', model);
  mock.verify();
});

On utilise la méthode twice avec un autre mock.

J'espère que vous commencez à comprendre la différence entre les mocks et les spies. Il y a cependant une autre grande partie de Sinon.JS, c'est l'API stub.

V. Stubs

En creusant davantage dans Backbone.gapiRequest, on constate que les requêtes sont censées avoir une méthode execute qui est appelée pour envoyer des données de l'API de Google. Les espions et les stubs permettent de tester cela en utilisant la méthode yieldsTo :

 
Sélectionnez
test('gapiRequest lance le callback', function() {
  var spy = sinon.spy();
  sinon.stub(Backbone, 'gapiRequest').yieldsTo('execute', spy);
  Backbone.sync('update', model);
 
  assert.ok(spy.calledOnce);
  Backbone.gapiRequest.restore();
});

Ce test lance les événements suivants :

  • Backbone.sync appelle Backbone.gapiRequest ;
  • Backbone.gapiRequest reçoit un objet avec une propriété execute, laquelle a été remplacée par un espion ;
  • Backbone.gapiRequest appelle cette méthode execute, on s'en assure par assert.ok(spy.calledOnce).

Rassembler ces idées peut être utile pour s'assurer que les callbacks success ou error sont déclenchés après qu'une requête est terminée :

 
Sélectionnez
test('Errors get called', function() {
  var spy = sinon.spy()
    , options = { error: spy }
    ;
 
  // Faire un stub de la méthode update interne qui vient généralement de Google
  sinon.stub(gapi.client.tasks.tasks, 'update').returns({
    execute: sinon.stub().yields(options)
  });
 
  // Invoquer sync avec un faux modèle et des options de callback error
  Backbone.sync('update', model, options);
 
  assert.ok(spy.calledOnce);
  gapi.client.tasks.tasks.update.restore();
});

Ce test assure que error est appelé en utilisant un espion, et il fait également un stub sur gapi.client.tasks.tasks.update avec notre propre objet. Cet objet a une propriété d'exécution qui provoque le callback à s'exécuter à l'intérieur de gapiRequest et, finalement, à appeler error.

VI. Éclaircissement

J'ai écrit une suite de tests pour les tâches. Elle est basée sur des tests du tutoriel précédent donc il n'y a pas vraiment quelque chose de nouveau en dehors de la méthode teardown :

 
Sélectionnez
setup(function() {
  /// ...
 
  spyUpdate = sinon.spy(gapi.client.tasks.tasks, 'update')
  spyCreate = sinon.spy(gapi.client.tasks.tasks, 'insert')
  spyDelete = sinon.spy(gapi.client.tasks.tasks, 'delete')
 
  // ...
});
 
teardown(function() {
  gapi.client.tasks.tasks.update.restore();
  gapi.client.tasks.tasks.insert.restore();
  gapi.client.tasks.tasks.delete.restore();
});

J'ai trouvé que cette méthode est mieux que d'appeler reset, car il est facile d'essayer d'examiner les objets plus d'une fois lorsque plusieurs fichiers de tests sont chargés.

VII. Rédiger de bons tests avec Sinon.JS

Sinon.JS pourrait ressembler à une petite bibliothèque que vous pouvez déposer dans Mocha, Jasmine ou QUnit, mais il y a un art d'écrire de bons tests Sinon.JS. La documentation de Sinon.JS donne des explications quand les spies, les mockes et les stubs sont utiles, mais il y un facteur subjectif en jeu, en particulier lorsqu'il s'agit de décider si un test est mieux écrit avec un mock ou un stub.

Voici quelques conseils que j'ai trouvé utiles :

  • les espions sont parfaits pour les fois où vous voulez tester l'application entière plutôt qu'une classe spécifique ou une méthode ;
  • les stubs deviennent maniables quand il existe des méthodes que vous ne voulez pas exécuter ou dont vous souhaitez forcer l'exécution ;
  • les mocks sont bons pour tester des méthodes spécifiques ;
  • un mock unique pour chaque cas doit être utilisé ;
  • vous devez utiliser restore() après avoir utilisé des espions et des stubs, on oublie facilement et ça provoque des erreurs.

VIII. Résumé

Stylistiquement, ils sont tous trois très différents mais ils paraissent semblables jusqu'à ce que vous ayez un peu de pratique avec Sinon.JS. Il y a des discussions sur les mocks et les stubs sur le groupe Google de Sinon.JS et donc il est probablement préférable de demander à Christian sur ce groupe si vous avez du mal faire ce que vous voulez avec Sinon.JS.

Le code source de ce tutoriel se trouve ici : alexyoung / dailyjs-backbone-tutorial en version 0c6de32.

IX. Remerciements

Cet article a été publié avec l'aimable autorisation de Alex Young. L'article original peut être lu sur le site DailyJSDailyJS : Backbone.js Tutorial: Testing with MocksBackbone.js Tutorial: Testing with Mocks.
Je remercie également FirePrawn pour sa relecture attentive et assidue.