Click here to Skip to main content
15,887,972 members
Articles / Web Development / ASP.NET

Write DOM-less Unit Tests in JavaScript

Rate me:
Please Sign up or sign in to vote.
4.75/5 (4 votes)
1 May 2015CPOL5 min read 13.1K   8   1
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.

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:

JavaScript
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:

JavaScript
{
    "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:

Basic grid line of business application

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:

JavaScript
@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:

JavaScript
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:

JavaScript
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:

JavaScript
(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:

JavaScript
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:

JavaScript
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:

JavaScript
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:

Passing tests

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Engineer
United States United States
Husband, father, and software engineer living in Houston Texas. Passionate about JavaScript, C#, and webbing all the things.

Comments and Discussions

 
QuestionExcellent Article! Pin
Your Display Name Here6-May-15 12:21
Your Display Name Here6-May-15 12:21 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.