Suite à l’article 1/3 je vais montrer comment faire des tests web fonctionnels avec le formalisme BDD à l’aide de Cucumber-js et l’implémentation du composant Page Object avec Zombie.js.

A la différence de CasperJS qui s’appuie sur PhantomJS pour l’exécution des tests, Zombie.js s’exécute dans Node.js.

On obtient donc la stack de test :

  • Zombie.js + Cucumber-js sur Node.js
  • Versus CasperJS sur PhantomJS dans l’article précédent

L’exemple complet en CucumberJs + ZombieJs est disponible sur Github sur la branche zombiejs-it-tests (voir README pour l’exécution des tests).

Composant Page Object avec Zombie.js

function TodoPage() {
'use strict';
var ENTER_KEY = 13;
var idNew = '#new-todo';
var title = 'title';
var titleList = '#header > h1';
var children = '#todo-list li';
var childrenVisible = '#todo-list li:not(.hidden)';
var nthChild = function(n) {
return children+':nth-child('+n+')';
};
var nthChildAndSelector = function(selector,n) {
return nthChild(n) + selector;
};
var enterKey = function(selector) {
browser.evaluate("var e = $.Event('keypress');e.which = "+ENTER_KEY+";$('"+selector+"').trigger(e);");
};
var nthLabel = _.partial(nthChildAndSelector,' label');
var nthInput = _.partial(nthChildAndSelector,' input.edit');
var nthDestroyBtn = _.partial(nthChildAndSelector,' button.destroy');
var nthCheckbox = _.partial(nthChildAndSelector,' input.toggle');
var nthCompleted = _.partial(nthChildAndSelector,'.completed');
var nthHidden = _.partial(nthChildAndSelector,'.hidden');
var nthNotHidden = _.partial(nthChildAndSelector,':not(.hidden)');
var browser = new Browser();
this.before = function(done){
browser.visit(config.url,done);
browser.evaluate("window.localStorage.clear();");
};
this.title = function() {
return browser.text(title);
};
this.titleList = function() {
return browser.text(titleList);
};
this.typeNew = function(newTodo) {
browser.fill(idNew, newTodo);
return this;
};
this.enterNew = function() {
enterKey(idNew);
};
this.nbVisible = function() {
return browser.querySelectorAll(childrenVisible).length;
};
this.nthText = function(nth) {
return browser.text(nthLabel(nth));
};
this.mouseOverNth = function(nth) {
browser.fire(nthChild(nth),'mouseover');
return this;
};
this.deleteNth = function(nth) {
browser.click(nthDestroyBtn(nth));
};
this.done = function(nth) {
browser.click(nthCheckbox(nth));
};
this.nthCompleted = function(nth) {
return _.isElement(browser.querySelector(nthCompleted(nth)));
};
this.doubleClickNth = function(nth) {
browser.fire(nthLabel(nth),'dblclick');
};
this.editNth = function(nth,todo) {
browser.fill(nthInput(nth),browser.text(nthLabel(nth))+todo);
return this;
};
this.enterNth = function(nth) {
enterKey(nthInput(nth));
};
this.first = _.partial(this.nthText,1);
this.mouseOverFirst = _.partial(this.mouseOverNth,1);
this.deleteFirst = _.partial(this.deleteNth,1);
this.doubleClickFirst = _.partial(this.doubleClickNth,1);
this.editFirst = _.partial(this.editNth,1);
this.enterFirst = _.partial(this.enterNth,1);
}
module.exports = new TodoPage();
view raw todoPage-zombiejs.js hosted with ❤ by GitHub
  • Contient tous les appels à l’API Zombie.js
  • Encapsule les sélecteurs CSS
  • Expose les fonctions pour lancer les actions sur le SUT (System Under Test)
  • Expose les fonctions pour récupérer les informations sur l’état du SUT

Ecriture des TI avec Mocha / Chai, exemple de l’ajout des todos

describe('Todo add scenario', function() {
before(function(done) {
todo.before(done);
});
it('titre de la page', function() {
expect(todo.title()).to.equal('TodoListWithBB');
});
it('titre de la todo list', function() {
expect(todo.titleList()).to.equal('todos');
});
it('ajout premier dans la todo list',function() {
todo.typeNew('first todo').enterNew();
expect(todo.first()).to.equal('first todo');
});
it('ajout deuxieme todo dans la todo list',function() {
todo.typeNew('second todo').enterNew();
expect(todo.nbVisible()).to.equal(2);
expect(todo.nthText(2)).to.equal('second todo');
});
});
view raw todoAdd-zombiejs.js hosted with ❤ by GitHub
  • Description de la suite avec la syntaxe Mocha
  • Lance les actions par l’intermédiaire du Page Object
  • Effectue les vérifications avec les assertions Chai

Scénario de l’ajout en BDD

Feature: todo add feature
Scenario: add todos in todo list
When je saisis le todo "first todo"
And je saisis le todo "second todo"
Then le todo "first todo" est placé en position 1 dans la liste
And le todo "second todo" est placé en position 2 dans la liste
view raw todoAdd.feature hosted with ❤ by GitHub

Implémentation des step Cucumber-js pour faire la liaison avec le BDD

module.exports = function() {
this.Before(function (callback) {
todo.before(callback);
});
this.Given(/(\d+) todos dans la liste/, function (nbTodos, callback) {
for(var i=1;i<=parseInt(nbTodos);i++) {
todo.typeNew("todo "+i).enterNew();
}
callback();
});
this.When(/je saisis le todo "(.*)"/, function (aTodo, callback) {
todo.typeNew(aTodo).enterNew();
callback();
});
this.Then(/le todo "(.*)" est placé en position (\d+) dans la liste/, function (aTodo, position, callback) {
expect(todo.nthText(position)).to.equal(aTodo);
callback();
});
this.When(/je supprime le todo placé en position (\d+) dans la liste/, function (position, callback) {
todo.mouseOverNth(position).deleteNth(position);
callback();
});
this.Then(/la liste contient (\d+) todo\(s\)/, function (nbTodos, callback) {
expect(todo.nbVisible()).to.equal(parseInt(nbTodos));
callback();
});
this.When(/je coche le "(premier|deuxième|troisième)" todo/, function (nth,callback) {
todo.done(nthToPosition(nth));
callback();
});
this.When(/je décoche le "(premier|deuxième|troisième)" todo/, function (nth,callback) {
todo.undo(nthToPosition(nth));
callback();
});
this.When(/je coche tous les todos/, function (callback) {
todo.doneAll();
callback();
});
this.When(/je décoche tous les todos/, function (callback) {
todo.undoAll();
callback();
});
this.Then(/le "(premier|deuxième|troisième)" todo est fait/, function (nth,callback) {
expect(todo.nthCompleted(nthToPosition(nth))).to.be.true;
callback();
});
this.Then(/le "(premier|deuxième|troisième)" todo est à faire/, function (nth,callback) {
expect(todo.nthCompleted(nthToPosition(nth))).to.be.false;
callback();
});
this.When(/j'édite le "(premier|deuxième|troisième)" todo avec la valeur "(.*)"/, function (nth,valeur,callback) {
var position = nthToPosition(nth);
todo.doubleClickNth(position);
todo.editNth(position,valeur).enterNth(position);
callback();
});
function nthToPosition(nth) {
return ['premier','deuxième','troisième'].indexOf(nth)+1
}
};
  • Instance Cucumber this fournie les fonctions Given, When, Then
  • Given génère des todos en tant que pré-requis de certains scénarios
  • When lance les actions par l’intermédiaire du Page Object
  • Then effectue les vérifications avec les assertions Chai

Limitations de Zombie.js

  • Ne s’exécute pas dans un browser standard
  • Documentation succinte
  • Recours à l’exécution d’expressions JS pour certaines actions
  • Click sur les filtres des todos gérés par le routeur Backbone.js non pris en compte
  • Pas de screenshot pour le debug

Cas d’usage possible

  • Tests fonctionnels BDD headless vraiment « Insanely fast » (comme le dit le slogan de Zombie.js)
  •   Mais pas des tests vraiment End To End car n’utilise pas un vrai browser et risque de ne pas toujours reproduire fidèlement le comportement du end user
  • Remarque : l’écriture des TI en plus du BDD est présentée ici à titre de comparaison de l’article 1 mais n’est pas toujours indispensable dans un projet réel

Pour réaliser des tests BDD vraiment orientés End to End avec les browsers utilisés par les utilisateurs finaux on va utiliser la techno Selenium dans le dernier article de la série.