Nous avons découvert dans un précédent article — intitulé Préparation aux tests unitaires d’une application AngularJS — la mise en place de tests unitaires au sein d’une application AngularJS.
Aujourd’hui nous allons aborder la partie tests fonctionnels, plus communément appelés tests E2E (End to End).
Le but des tests E2E est de lancer l’application dans un navigateur web pour obtenir les mêmes comportements qu’un utilisateur lambda. Ces tests sont utiles pour vérifier les interactions côté client, ainsi que le bon retour lors d’une action.
L’avantage des tests E2E est de pouvoir tester son application comme elle apparaîtra aux yeux de l’utilisateur.
Ils sont ceux qui collent le mieux pour répondre parfaitement à une demande client. Ex: Lors du click sur le bouton 'acheter' afficher une image 'panier'
.
Mais ils permettent également de maintenir cohérente l’application. Car tout changement au sein de l’application permet de vérifier la conformité de l’application lors des évolutions de versions.
Protractor permet d’exécuter ces tests E2E directement dans le ou les navigateurs physiques de votre choix. Il interagit avec le navigateur naturellement comme tout utilisateur le ferait.
Nous partons du principe que vous avez déjà une application AngularJS mise en place.
Il nous faut avant tout installer le package protractor
.
npm install -g protractor
Nous utiliserons ici Mocha, qui est un framework de tests JavaScript. C’est un choix personnel car ce framework a une syntaxe proche de celle de Rspec
(Ruby), donc un style d’écriture des tests orienté BDD.
npm install mocha --save-dev
Pour la librairie d’assertions JS, mon choix s’est porté sur ChaiJS qui a une syntaxe que j’affectionne et qui en plus est suggéré lors de l’utilisation de Mocha
.
npm install chai --save-dev
L’utilisation de Mocha
avec Protractor
requiert l’utilisation du package chai-as-promised
pour que nos tests puissent se jouer.
npm install chai-as-promised --save-dev
Il nous reste maintenant le principal, Selenium
, c’est une application qui va faire le lien entre nos tests et le navigateur sur lequel nos tests sont joués.
Nous pouvons télécharger le binaire avec cette commande :
webdriver-manager update
Il se peut que vous ne trouviez pas le binaire webdriver-manager
, dans ce cas il est disponible dans les node_modules
de Protractor (./node_modules/protractor/bin/webdriver-manager
).
Ce fichier de configuration n’est pas obligatoire, mais très largement conseillé, car nous allons pouvoir spécifier à Protractor les variables de configuration suivantes :
Ce fichier de configuration a un nommage libre, personnellement je préfère le nommer protractor-config.js
pour lever toute ambiguïté.
Deux cas de figure ici, car il y a deux façons d’appeler Selenium dans notre fichier de configuration. Tout va dépendre de la manière de lancer nos tests.
Dans le premier cas, nous partons du principe que nous lançons Selenium en arrière plan avant d’exécuter nos tests. Il faut alors préciser dans le fichier de configuration, quelle est l’URL correspondante.
exports.config = {
seleniumAddress: 'http://localhost:4444/wd/hub',
…
};
Dans l’autre cas, nous demandons à Protractor
de lancer Selenium via le pilote selenium-webdriver
avant d’exécuter les tests.
exports.config = {
seleniumServerJar: "./node_modules/protractor/selenium/selenium-server-standalone-2.48.2.jar",
…
};
À vous de choisir la solution qui s’adaptera le mieux à votre besoin.
Protractor se lance par défaut avec le framework Jasmine, or nous utilisons dans ce cas-ci Mocha. À nous de lui préciser que c’est ce framework qui lance nos tests.
exports.config = {
framework: "mocha",
…
};
La configuration de Protractor permet de les jouer sur un ou plusieurs navigateurs.
Dans le cas où vous souhaitez les lancer sur un seul navigateur, vous pouvez utiliser cette configuration.
exports.config = {
capabilities: {
'browserName': 'chrome'
},
…
};
Mais si vous souhaitez pouvoir les exécuter sur Chrome et Firefox, la configuration suivante permet de spécifier autant de navigateurs que vous le souhaitez.
exports.config = {
multiCapabilities: [{
browserName: 'firefox'
}, {
browserName: 'chrome'
}],
…
};
Reste la dernière étape de configuration pour que tout soit valide, il nous faut maintenant spécifier où se trouvent nos fichiers de tests.
exports.config = {
specs: ["test/e2e/**/*Spec.js"],
…
};
Cette configuration va nous permettre de récupérer tous les fichiers se trouvant dans le dossier ainsi que les sous-dossiers de test/e2e/
et se terminant par *Spec.js
.
Si vous souhaitez lui spécifier un seul fichier vous le pouvez :
exports.config = {
specs: ["test/e2e/WelcomeSpec.js"],
…
};
Comme cité plus haut, nous utilisons Mocha et ChaiJS pour écrire nos tests, cela nécessite une configuration en plus sur nos fichiers.
var chai = require('chai');
var chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
var expect = chai.expect;
describe('Welcome page', function() {
});
Ici, ChaiJS va remplacer le expect
fourni par Protractor par celui issu de chai-as-promised
.
La suite de l’article contient les points qui m’ont semblé important lorsque j’ai débuté avec Protractor. Pour accéder à la référence de toutes les interactions possibles avec Protractor, voici son API.
Pour que notre test soit fonctionnel, il faut exécuter les actions comme un utilisateur le ferait.
Ici nous utilisons browser.get()
pour accéder à la page de notre application par le biais du navigateur.
eventually
est utilisé car l’assertion est issue d’une promise.
…
describe('Welcome page', function() {
it('has a correct title', function() {
browser.get("http://locahost:2222");
expect(browser.getTitle()).to.eventually.equal("My welcome page");
});
});
Dans cet exemple nous voulons vérifier que le titre de notre application est correct.
browser.getTitle()
nous retourne le contenu de la balise <title></title>
.
L’un des points sur lequel j’ai bloqué lors de mes débuts avec Protractor est le mock d’appels HTTP.
C’est l’une des choses primordiales pour lancer nos tests dans un environnement cloisonné, il serait absurde de devoir lancer une application tierce pour qu’ils puissent tourner correctement.
Nous surchargeons l’application AngularJS lors de nos tests pour lui spécifier que plusieurs routes doivent nous retourner un contenu prédéfini.
describe('Welcome page', function() {
beforeEach(function() {
browser.addMockModule('httpMocker', function() {
angular.module('httpMocker', ['welcomeApp', 'ngMockE2E'])
.run(function($httpBackend) {
$httpBackend.whenGET(/api\/cars).respond({cars: ["renault"]});
$httpBackend.whenGET(/.*/).passThrough();
});
});
});
…
});
Cet exemple est placé volontairement à l’intérieur d’un beforeEach
, cela nous permet de mocker
nos requêtes, sur tous les tests à l’intérieur du block Welcome page
.
Un point important ici est l’utilisation du $httpBackend.whenGET(/.*/).passThrough();
, cela précise à notre application que tous les appels en dehors de ceux spécifiés se déroulent normalement. Si vous omettez ce passThrough()
, plus aucune de vos routes ne sera accessible.
Il est conseillé d’utiliser une regex pour spécifier la route mockée, sur notre exemple notre regex correspond à la route http://monapp/api/cars
.
La récupération d’un élément s’opère par l’intermédiaire de la fonction element
.
Nous utilisons ici le by.css()
pour lui spécifier que nous souhaitons trouver l’élément par l’intermédiaire d’une requête sur le DOM
.
it('has submit button', function() {
browser.get("http://locahost:2222");
var submitButton = element(by.css("form input[type=submit]"));
expect(submitButton.getAttribute('value')).to.eventually.equal("Add to cart");
});
Il existe évidemment bon nombre de façon de récupérer un ou des éléments, je vous laisse le soin de les découvrir sur la documentation de référence Protractor By.
Imaginons que notre application contienne plusieurs span
avec chacune du texte brut. Nous voulons savoir sans distinction des span
si un texte est bien présent à l’intérieur de l’une d’elles.
Rien de plus simple, il nous suffit d’utiliser element.all()
en lieu et place de element()
.
it('has cart text', function() {
browser.get("http://locahost:2222");
var spanElements = element.all(by.css("span"));
expect(span.getText()).to.eventually.include("My cart is empty");
});
Nous souhaitons vérifier si lors d’un click sur un bouton, son texte change. il nous suffit d’appeler click()
sur l’élément récupéré.
it('has submit button', function() {
browser.get("http://locahost:2222");
var submitButton = element(by.css("form input[type=submit]"));
submitButton.click();
expect(submitButton.getAttribute('value')).to.eventually.equal("Added !");
});
C’est l’une des choses que l’on est quasiment sûr d’utiliser dans un de nos tests, nous utilisons dans ce cas la fonction sendKeys()
sur l’élément sélectionné.
it('returns error when email is invalid', function() {
var errorMessage = "Wrong email";
var submitButton = element(by.css("form input[type=submit]"));
var inputEmail = element(by.css("form input.email"));
inputEmail.sendKeys("test@synbioz.com");
submitButton.click();
expect(element(by.css("span.error-message")).getText()).to.eventually.equal(errorMessage);
});
sendKeys()
permet également d’envoyer un signal d’appui sur une touche. Si nous prenons pour exemple la touche entrée
: .sendKeys(protractor.Key.ENTER)
.
Il ne nous reste plus qu’à lancer nos tests. Nous utilisons Protractor et son fichier de configuration pour les lancer.
protractor protractor-config.js
Si vous avez tout configuré correctement, vous verrez votre navigateur choisi dans la configuration s’ouvrir et exécuter vos tests.
Protractor est donc l’un des outils primordiaux à avoir dans votre stack de développement d’application AngularJS. Cela apporte une réelle vision par les tests du rendu vu par l’utilisateur.
N’hésitez pas à creuser dans l’API Protractor pour y découvrir tous les matchers utiles à vos développements.
Vous pouvez également vous intéresser à Grunt/Gulp pour exécuter Protractor en tant que tâche, comme par exemple gulp matache-e2e
.
L’équipe Synbioz. Libres d’être ensemble.