Introduction
Writing JavaScript can be daunting. What begins with fun light scripting, quickly escalates into a tangled mess. I once found myself in an unimaginative hodgepodge of callbacks tight coupled to HTML. But then, I began to believe that there must be a better way. In this article, I would like to explore this better way by writing unit tests in JavaScript.
I’ve prepared a demo to illustrate this methodology. It consists of a typical line of business grid with filtering. Nothing fancy but conveys what I see in professional web solutions.
Tooling and Set Up
I will use ASP.NET MVC for the back-end. For this demo, it is a simple data source so I will not be covering the back-end in this article.
For the front-end, I will use Grunt, and Mocha to write unit tests. The tooling runs on Node.js so be sure to have it installed in your machine. You can find instructions for getting these set up on their sites, so I will not be covering that here. Needless to say, you will need the grunt-cli
npm package in your global packages.
Here is the Gruntfile.js:
module.exports = function (grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
simplemocha: {
all: {
src: ['Scripts/spec/**/*.js'],
options: {
ui: 'bdd',
reporter: 'spec'
}
}
}
});
grunt.loadNpmTasks('grunt-simple-mocha');
grunt.registerTask('default', ['simplemocha']);
};
And here is the package.json
:
{
"name": "WriteDomLessJavaScriptUnitTests",
"version": "0.1.0",
"devDependencies": {
"grunt": "~0.4.5",
"grunt-simple-mocha": "~0.4.0",
"should": "~5.2.0"
}
}
We are all set to write unit tests. Type npm install
in the console and you should be good to go. Make sure you are in the same path as the config files.
The User Interface
Before I begin, I would like you to start thinking about this problem. The grid looks like this:
Nothing that will win design competitions. Our job is to make the dropdown and button respond to user interaction. The dropdown filters by department and retrieve refreshes the data via Ajax. This is what Razor looks like:
@model ProductAllViewModel
@{
ViewBag.Title = "Demo";
var departments = Model
.Departments
.Select(d => new SelectListItem { Value = d.Id.ToString(), Text = d.Name })
.ToList();
departments.Insert(0, new SelectListItem { Value = "0", Text = "All" });
}
@Html.DropDownList("departments", departments)
<button id="retrieve" data-url="@Url.Action("Index", "Product")">Retrieve</button>
<p>List of products:</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
<th>Department</th>
</tr>
</thead>
<tbody id="products"
data-products="@Json.Encode(Model.Products)"
data-departments="@Json.Encode(Model.Departments)">
@foreach (var product in Model.Products)
{
<tr data-department="@product.DepartmentId">
<td>@product.Name</td>
<td>@product.Price.ToString("c")</td>
<td>@Model.Departments.First(x => x.Id == product.DepartmentId).Name</td>
</tr>
}
</tbody>
</table>
As you can see, this is straight up HTML, without JavaScript. I am keeping everything separate for a very good reason.
Separate Concerns
Looking at the HTML, can you imagine what the JavaScript is going to look like? I placed the list of products right in the data-products
attribute inside #products
. Notice I use id
attributes as sort of pointer references. The idea is to make life easy for me when I need to find specific elements in the DOM.
The basic principle is to think of the DOM as a sort of data repository. JavaScript gets decoupled from the DOM if you see HTML in this way. The beauty here is JavaScript can do well on its own without relying on DOM elements. We can re-imagine DOM elements as “data elements” and gain a new level of abstraction. This enables us to think about the problem in a new way.
Let’s begin by writing unit tests for filtering by department:
describe('A product module', function () {
it('filters by department', function () {
var list = [{ "DepartmentId": 1, "Name": "X" },
{ "DepartmentId": 2, "Name": "Y" }],
result = product.findByDepartmentId(list, 2);
result.length.should.equal(1);
result[0].Name.should.equal('Y');
result = product.findByDepartmentId(list, 0);
result.length.should.equal(2);
});
});
This unit test helps us think about the problem. The purpose of test driven design is to guide us in writing good code. Let’s go ahead and pass the test:
var product = (function () {
return {
findByDepartmentId: function (list, departmentId) {
return list.filter(function (prod) {
return departmentId === 0 || prod.DepartmentId === departmentId;
});
}
};
}());
if (typeof module === 'object') {
module.exports = product;
}
Notice I use module.exports so I can use this same code in Node.js without a browser. I’m using a simple filter()
in JavaScript to do the heavy lifting for me.
With this foundation in place, what about DOM events? Surely, we must respond to user interaction in some way. DOM events are so ubiquitous in JavaScript, they feel like the enemy at the gates.
DOM Events
Turns out, DOM elements don’t need in-line JavaScript to respond to events. One can do:
(function () {
var departmentSelect = document.getElementById('departments'),
retrieve = document.getElementById('retrieve'),
elProducts = document.getElementById('products');
if (departmentSelect) {
departmentSelect.addEventListener('change', function (e) {
var departmentId = parseInt(e.currentTarget.value, 10),
productList = JSON.parse(elProducts.dataset.products),
departmentList = JSON.parse(elProducts.dataset.departments),
filteredList = product.findByDepartmentId(productList, departmentId);
elProducts.innerHTML = product.renderTable(filteredList, departmentList);
});
}
}());
I’m using my new module product.findByDepartmentId()
to do the heavy lifting. This component has passing tests so I know it works. Notice the use of departmentSelect
to check for the existence of a DOM element. To check for existence, I use a truthy value inside if. If it finds it, it attaches the event dynamically. But what about product.renderTable()
? Surely, this must get coupled to the DOM in some way, correct? Let’s explore.
The DOM API
Turns out, all elProducts.innerHTML
needs is a string
. So, we can write this unit test:
it('renders a table', function () {
var products = [
{ "DepartmentId": 1, "Name": "X", "Price": 3.2 },
{ "DepartmentId": 2, "Name": "Y", "Price": 1.11 }],
departments = [{ "Id": 1, "Name": "A" }, { "Id": 2, "Name": "B" }],
html = '<tr><td>X</td><td>$3.20</td><td>A</td>' +
'</tr><tr><td>Y</td><td>$1.11</td><td>B</td></tr>',
result = product.renderTable(products, departments);
result.should.equal(html);
});
Now to pass the test:
renderTable: function (products, departments) {
var html = '';
products.forEach(function (p) {
var department;
departments.forEach(function (d) {
department = d.Id === p.DepartmentId ? d.Name : department;
});
html +=
'<tr>' +
'<td>' + p.Name + '</td>' +
'<td>$' + p.Price.toFixed(2) + '</td>' +
'<td>' + department + '</td>' +
'</tr>';
});
return html;
}
Ajax
It's time for big and scary Ajax, welcome to the boss level! What must I do to keep sound design principles? How about:
retrieve.addEventListener('click', function (e) {
var bustCache = '?' + new Date().getTime(),
oReq = new XMLHttpRequest();
elProducts.innerHTML = 'loading...';
oReq.onload = function () {
var data = JSON.parse(this.responseText),
departmentId = parseInt(departmentSelect.value, 10),
fileredList = product.findByDepartmentId(data.Products, departmentId);
elProducts.innerHTML = product.renderTable(fileredList, data.Departments);
};
oReq.open('GET', e.currentTarget.dataset.url + bustCache, true);
oReq.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
oReq.send();
});
Turns out, there are no new modules necessary. The testable module product has a very specific concern, so I am able to reuse it. The beauty here is that the product is not tied to any part of the DOM, so this gives me complete freedom.
Now fire up grunt in the console and watch for the green lights:
Not sure if it's clear from the image above, but each test clearly labels what the module does. “A product filters by”, for example, this is self-documenting code. Just by running the tests in this solution, I am able to pick out what the front-end does. No need to go spelunking through copious amounts of incomprehensibly bad code.
Conclusion
I hope you can see what a better alternative this is. In my mind, testable code is well written code. Especially when dealing with JavaScript. Oftentimes, I find myself in a time vacuum when I load the entire solution in the browser only to find an issue with JavaScript. So, having unit tests automated through Grunt is a huge productivity boost.
The idea behind unit tests is to spend less time starring at an empty void, and more time writing sound code. If interested, you can find the entire solution up on GitHub.
The post Write DOM-less Unit Tests in JavaScript appeared first on BeautifulCoder.NET.