Les tests automatisés en front-end ont longtemps été ignorés, et le sont encore trop.
Écrire des tests peut paraître chiant, mais le temps qu'ils rapportent compense largement celui que l'on passe à les écrire.
Prenons un exemple simple et moche :
//app.js
(function() {
var cart = $("#cart");
function addToCart(id) {
var element = $("<li></li>");
element.html(catalog[id]);
cart.append(element);
}
$(".js-updateCart").on("click", function(eventObject) {
addToCart($(this).data("id"));
});
})();
Tester ce code est particulièrement lourd, pour plusieurs raisons :
click
pour tester un comportement logique.data-id
à chaque cas souhaité.addToCart
on doit aller regarder dans le DOM.Vous pouvez utiliser un namespace adéquat.
var app = (window.app = {});
app.cart = {};
app.cart.addToCart = function() {
/* … */
};
Mais ne souhaitez probablement pas rendre vos méthodes accessibles à la console.
Utilisez un module-system : browserify ou
requirejs (si vous n'avez pas besoin de chargement
conditionnel et souhaitez créer un bundle par build, utilisez browserify qui
possède une syntaxe beaucoup plus sympathique et une codebase réduite, vous
pourrez même require
depuis un module npm
comme on vous l'explique dans un article dédié).
var $ = require("jquery"); // oh, un module npm
module.exports = {
element: $("#cart"),
addToCart: function() {
/* … */
},
};
Vous pourrez ainsi séparer proprement votre application en modules et les tester individuellement.
Là, ça devient un peu plus tricky. Ce que je conseille à titre personnel c'est d'utiliser des events pour faire communiquer les différentes parties de l'application.
Cela permet d'éviter d'utiliser des mocks d'autres parties de l'application partout.
Par exemple, si je souhaite tester le fait qu'un click sur .js-updateCart
déclanchera bien updateCart
:
//view.js
// view est une petite class qui route les events
// en rendant les listeners accessibles aux tests
// vous pouvez très facilement en concevoir une
// à votre goût en quelques lignes de code
var view = require("../lib/view");
// eventbus est l'event bus de l'application, il orchestre
// la communication entre les différents modules
var eventbus = require("../eventbus");
var $ = require("jquery");
module.exports = view.extend({
element: document.body,
events: [
{
type: "click",
selector: ".js-addToCart",
listener: "addToCart",
},
],
addToCart: function(eventObject) {
var target = eventObject.currentTarget;
var id = $(target).data("id");
eventbus.fire("addToCart", {
id: id,
});
},
});
et je n'aurais qu'à écouter cet event depuis cart
:
// cart.js
var eventbus = require("../eventbus");
var catalog = require("../catalog");
module.exports = {
initialize: function() {
this._addToCart = this.addToCart.bind(this);
eventbus.listen("addToCart", this._addToCart);
},
release: function() {
eventbus.stopListening("addToCart", this._addToCart);
},
addToCart: function(eventObject) {
// et on a eventObject.id
this.products.push(catalog[eventObject.currentTarget.data("id")]);
},
};
Grâce à cette architecture, je vais pouvoir tester individuellement les deux modules.
// view.test.js
var tape = require("tape");
var view = require("../app/view");
var eventbus = require("../eventbus");
var $ = require("jquery");
tape("view", function(test) {
test.plan(1);
var element = $("<div></div>");
element.data("id", 1);
// on teste facilement l'envoi
eventbus.listen("addToCart", function(eventObject) {
test.equal(eventObject.id, 1);
});
view.addToCart({
currentTarget: element,
});
});
// cart.test.js
var tape = require("tape");
var cart = require("../app/cart");
var eventbus = require("../eventbus");
var catalog = require("../catalog");
tape("cart", function(test) {
cart.initialize();
eventbus.fireSync("addToCart", { id: 1 });
test.deepEqual(cart[0], catalog[1], "receives addToCart event");
test.end();
});
Ce sont souvent les edge-cases qui nous font nous arracher les cheveux. Pour pallier ces soucis, tester en profondeur est essentiel.
Prenons un exemple, une méthode qui teste si la valeur qu'on lui passe est une
string
.
On serait tenté de simplement tester les résultats positifs de cette façon :
tape("type.isString on strings", function(test) {
test.equal(type.isString(""), true);
test.end();
});
Or, si par mégarde ma méthode isString
se révèle être :
type.isString = function(value) {
return typeof value == "string";
};
on ne vérifie pas le cas type.isString(new String(""))
.
Du coup, des tests complets :
tape("type.isString on strings", function(test) {
test.equal(type.isString(""), true);
test.equal(type.isString(new String("")), false);
test.end();
});
permettent ici d'identifier qu'en réalité on a besoin de :
type.isString = function(value) {
return Object.prototype.toString.call(value) == "[object String]";
};
Et c'est à la force des résultats de tests que vous identifierez rapidement vous familiariserez à tous ces edge-cases.
À chaque correctif de bug, ajoutez des tests (mais ça, vous le saviez déjà).
À l'heure actuelle, où l'on se trouve notamment très souvent avec du feature testing. Pour le tester complètement, on doit se servir de vrais navigateurs : un headless (certes, très pratique) comme phantomjs ne permettra pas d'obtenir 100% de coverage.
var supportsAnimationFrame = !!(
win.requestAnimationFrame ||
win.webkitRequestAnimationFrame ||
win.mozRequestAnimationFrame ||
win.ORequestAnimationFrame ||
// vous avez fait une faute de frappe,
// et mis un `O` majuscule, seul un
// test sur les version concernées d'Opera
// vous permettront de l'identifier
win.msRequestAnimationFrame
);
De plus, vous pouvez par habitude être tenté d'utiliser un
Array.prototype.forEach
alors que votre scope navigateur inclut IE8; seuls des
tests sur un vrai browser vous permettront d'identifier le souci.
Je vous conseille fortement testling, très simple à intégrer dans vos modules, et gratuit pour tout projet open-source.
Dans le package.json
:
{
"testling": {
"files": "test/**/*.js",
"browsers": [
"ie/9..latest",
"chrome/22..latest",
"firefox/16..latest",
"safari/6..latest",
"opera/11.0..latest",
"iphone/6..latest",
"ipad/6..latest",
"android-browser/latest"
]
}
}
Et ajouter un webhook dans l'admin de votre repository GitHub pointant vers
Vous pouvez l'utiliser avec tape (son test harness par défaut) Mocha, QUnit et
n'importe quelle bibliothèque de test supportant TAP
comme indiqué sur
le site de testling.
Vous trouverez aussi des alternatives populaires similaires, ou ayant une approche différente.
Par ailleurs, si dans votre code il existe des variations entre les navigateurs, vous pouvez utiliser du test conditionnel :
if (typeof {}.__proto__ == "object") {
test.equal(
list.__proto__ === Array.prototype,
false,
"__proto__ isn't Array.prototype",
);
} else {
test.equal(list instanceof Array, false, "Isn't a window Array");
}
Happy testing!
$ testling
TAP version 13
# tests
ok 1 bisous
1..1
# tests 1
# pass 1
# ok