📜 ⬆️ ⬇️

Writing testable javascript

[Approx. transl.]: I bring to your attention a translation of an article by Ben Cherry, a Twitter developer in the past. In this article, he gives some tips on writing javascript code suitable for testing.

The development culture on Twitter requires writing tests. I had no experience testing javascript before working on Twitter, so I had to learn a lot. In particular, some programming patterns that I used to use, which I wrote about and called for their use, turned out to be unsuitable for testing. So I thought that it was worth sharing some of the most important principles that I developed for writing testable Javascript code. The examples I give are based on QUnit , but can be applied to any Javascript testing framework.

Avoid singletons


One of my most popular posts was how to use the javascript Module template to create singletones in your application. This approach can be simple and useful, but it creates problems for testing for one simple reason: Singleton pollutes the state of an object between tests. Instead of a singleton as a module, you should create it as a constructed object and assign it to a global-level instance during initialization of your application.

For example, consider the following singleton module (an example, of course, made-up):
')
var dataStore = (function() { var data = []; return { push: function (item) { data.push(item); }, pop: function() { return data.pop(); }, length: function() { return data.length; } }; }()); 

In this module, we may want to test some methods. Here is an example of a simple QUnit test:

 module("dataStore"); test("pop", function() { dataStore.push("foo"); dataStore.push("bar") equal(dataStore.pop(), "bar", "popping returns the most-recently pushed item"); }); test("length", function() { dataStore.push("foo"); equal(dataStore.length(), 1, "adding 1 item makes the length 1"); }); 

During the execution of this test suite, the test of the “length” method will fail, but looking at it, it is not clear why. The problem is that the state of the dataStore object is preserved after the previous test. Simply reordering the tests will make both tests pass the test, which is an obvious sign that something has been done wrong. We can fix this by returning the state of the dataStore object before each test, but this means that we will have to constantly maintain a template for testing if we make changes to the module. It is better to use a different approach:

 function newDataStore() { var data = []; return { push: function (item) { data.push(item); }, pop: function() { return data.pop(); }, length: function() { return data.length; } }; } var dataStore = newDataStore(); 

Now the test suite looks like this:

 module("dataStore"); test("pop", function() { var dataStore = newDataStore(); dataStore.push("foo"); dataStore.push("bar") equal(dataStore.pop(), "bar", "popping returns the most-recently pushed item"); }); test("length", function() { var dataStore = newDataStore(); dataStore.push("foo"); equal(dataStore.length(), 1, "adding 1 item makes the length 1"); }); 

This approach allows our global object to behave, just as it did before, and the tests will not clog each other. Each test has its own copy of the dataStore object, which will be destroyed by the garbage collector after the test is completed.

Avoid creating private properties with closures.

Another pattern promoted by me is the creation of truly private properties in Javascript. The advantage of this method is that you can keep the global namespace free from unnecessary references to hidden implementation details. However, the misuse of this programming pattern can lead to unsuitable code for testing. The reason for this phenomenon is that your test suite does not have access to private functions hidden in closures, and therefore cannot test them. Consider an example:

 function Templater() { function supplant(str, params) { for (var prop in params) { str.split("{" + prop +"}").join(params[prop]); } return str; } var templates = {}; this.defineTemplate = function(name, template) { templates[name] = template; }; this.render = function(name, params) { if (typeof templates[name] !== "string") { throw "Template " + name + " not found!"; } return supplant(templates[name], params); }; } 

The key method in the Templater object is “supplant”, but we don’t have access to it outside the function closure. Thus, we cannot verify whether it works as planned. In addition, we cannot check if our defineTemplate method does anything without invoking the render method. We can add the “getTemplate ()” method, but then it turns out that we added a method to the public interface solely for testing purposes, which is not a good approach. In this situation, the construction of complex objects with important private methods will lead to the fact that you have to rely on untested code, which is quite dangerous. The following is an example of a testable version of this object:

 function Templater() { this._templates = {}; } Templater.prototype = { _supplant: function(str, params) { for (var prop in params) { str.split("{" + prop +"}").join(params[prop]); } return str; }, render: function(name, params) { if (typeof this._templates[name] !== "string") { throw "Template " + name + " not found!"; } return this._supplant(this._templates[name], params); }, defineTemplate: function(name, template) { this._templates[name] = template; } }; 

Here is the QUnit test suite for it:

 module("Templater"); test("_supplant", function() { var templater = new Templater(); equal(templater._supplant("{foo}", {foo: "bar"}), "bar")) equal(templater._supplant("foo {bar}", {bar: "baz"}), "foo baz")); }); test("defineTemplate", function() { var templater = new Templater(); templater.defineTemplate("foo", "{foo}"); equal(template._templates.foo, "{foo}"); }); test("render", function() { var templater = new Templater(); templater.defineTemplate("hello", "hello {world}!"); equal(templater.render("hello", {world: "internet"}), "hello internet!"); }); 

Please note that our test for the “render” method is only intended to make sure that the methods “defineTemplate” and “supplant” complement each other correctly. We have already tested them separately from each other, and this will allow us to easily understand which of the components is not working correctly if the test is not successful.

Write related functions

Related functions are important in any language, but Javascript has its own reasons for this. Much of what you do with Javascript uses global objects provided by the environment that your test suites rely on. For example, testing a function that changes a URL will be difficult, since all methods will be associated with window.location. Instead, you must break your system into logical components that will decide what to do next, and then write short functions that will do this. You can test logical functions on different incoming and outgoing data, and leave a function that modifies window.location untested. Provided that you have designed your system correctly, this approach will be safe.

Here is an example of a non-valid code:

 function redirectTo(url) { if (url.charAt(0) === "#") { window.location.hash = url; } else if (url.charAt(0) === "/") { window.location.pathname = url; } else { window.location.href = url; } } 

The logic in this example is quite simple, but we can imagine a more complex situation. With increasing complexity, we will not be able to test this method without redirecting the browser window.

And here is a good version:

 function _getRedirectPart(url) { if (url.charAt(0) === "#") { return "hash"; } else if (url.charAt(0) === "/") { return "pathname"; } else { return "href"; } } function redirectTo(url) { window.location[_getRedirectPart(url)] = url; } 

And now we can write a simple test suite for "_getRedirectPart":

 test("_getRedirectPart", function() { equal(_getRedirectPart("#foo"), "hash"); equal(_getRedirectPart("/foo"), "pathname"); equal(_getRedirectPart("http://foo.com"), "href"); }); 

In this case, the main part of the “redirectTo” method will be tested, and we can not worry about random redirects.

Write a lot of tests

This is no easy task, but it is very important to always remember this. Many programmers write too few tests, because writing them is a time-consuming exercise and takes a lot of time. I am constantly suffering from this problem, and so I wrote a small helper for QUnit, which will make writing tests a little easier. This is the “testCases ()” function, which you can call within the tested block, passing it a function, execution context and input / output data array for comparison. With it, you can easily create a test suite.

 function testCases(fn, context, tests) { for (var i = 0; i < tests.length; i++) { same(fn.apply(context, tests[i][0]), tests[i][1], tests[i][2] || JSON.stringify(tests[i])); } } 

Usage example:

 test("foo", function() { testCases(foo, null, [ [["bar", "baz"], "barbaz"], [["bar", "bar"], "barbar", "a passing test"] ]); }); 


findings

You can still write a lot about testing Javascript, and I am sure there are many good books on this topic, but I hope that I have given a good overview of the practical examples that I encounter in my daily work. I am not an expert in testing, so let me know if I made some mistake or give bad advice.

Source: https://habr.com/ru/post/237615/


All Articles