Ensuring code functionality in large web applications is quite a difficult task. Nowadays, lots of business logic is handled within the client and you have to make sure that everything works as intended. Maintenance can be very complex if you consider the fact that we currently have about 176,207 lines of JavaScript code and 20 frontend engineers working in different teams.
Imagine, for example, that we have seven sub-modules based on one main module. If you know change something in the main module, the changes could also affect the functionality of each sub-module and, in a worst-case scenario, break every module.
That’s why we write unit tests in our development process for each module to cover and test as much logic as possible. To achieve this we use QUnit as a testing framework. Besides using real assets for Ajax requests, we use Mockjax to mock requests and jQuery as a JavaScript Library
Simple code example
Let’s imagine that we would like to test a module. The module itself does nothing more than toggle (show/hide) an element while also sending out an Ajax request when checking a checkbox.
myModule.html
myModule.js
var myModule = function(container) { var container = $(container), button = container.find('[data-toggle="button"]'), toggleBox = container.find('[data-toggle="container"]'), checkbox = container.find('[data-checkbox="xhr"]'), init = function() { button.click(function() { toggleBox.toggle(); }); checkbox.change(function() { $.ajax("/foo/bar"); }); }; init(); return { init: init }; }; |
myModule_test.js
// Package separate tests into a module. module("myModule", { // Will be run before each test. setup: function () { // Get elements. this.container = $("#container"); this.button = this.container.find('[data-toggle="button"]'); this.toggleBox = this.container.find('[data-toggle="container"]'); this.checkbox = this.container.find('[data-checkbox="xhr"]'); // Mockjax general setup for each test. $.mockjaxClear(); $.mockjaxSettings.responseTime = 1; }, // Will be run after each test. teardown: function() { // Reset default values for future Ajax requests. $.ajaxSetup({ complete: $.noop }); } }); // Test to run. Tests are queued and run one after the other. test("check if toggleBox is toggled, when button is clicked", function() { // Specify number of assertions for this test. expect(3); // Initialize module new myModule(this.container); // Check original state of toggleBox (boolean assertion). ok(this.toggleBox.is(":visible"), "toggleBox is initially visible"); // Trigger click event on native button element. QUnit.triggerEvent(this.button[0], "click"); // Check state of toggleBox after clicking the button. ok(!this.toggleBox.is(":visible"), "toggleBox is hidden after button click"); QUnit.triggerEvent(this.button[0], "click"); ok(this.toggleBox.is(":visible"), "toggleBox is visible again after button click"); }); test("check if ajax request is done", function() { expect(1); // Wait for async request to run. stop(); // Mock ajax request with mockjax. $.mockjax({ url: "/foo/bar" }); new myModule(this.container); // Set default values for future Ajax requests. $.ajaxSetup({ complete: function() { ok(true, "triggered ajax request"); start(); } }); QUnit.triggerEvent(this.checkbox[0], "change"); }); |
QUnit Testresult
Conclusion
This example served to test the module’s logic. These tests can now be automated to be run repeatedly as regression tests so we can make sure that future changes don’t affect the basic functionality of this module.
We’ve also done this with over 2,700 unit tests which cover the client‐side logic across XING.com. These tests are integrated into Jenkins and run repeatedly. One of the important facts besides the sheer number of tests is test coverage. Covering 100% of a module is the key, because 100% coverage theoretically means zero false code and is hard to mess up. One can even use techniques like test‐driven development (TDD), which is an iterative process. First of all, a test case is written that will fail. Secondly, the developer produces code to pass that test. Using TDD highly depends on the situation because switching between writing a test and writing code does not always make sense.
As you now see, unit testing is a very important aspect in making sure that your software works. Nevertheless, you can’t completely rely on unit tests. They are a good indicator, but can’t replace real-life testing or cover every edge case that may occur.
Sascha Cacqueux works as a Frontend Engineer in the Growth Team at XING. He's keen on writing Javascript and unit-tests besides of semantic markup.
I wrote a blogpost some month ago with a similar approach for testing user interfaces. It’s nice to see that others have the same idea by testing and simulating user interaction with QUnit. You can find the blogpost here: http://michelgotta.posterous.com/user-interfaces-and-unittesting-with-qunit-an