Blog Article
9 Dec 15 1 min. read

A Taste Of Unit Testing For The Client-Side

An introduction to the concept of unit testing and writing testable code. ![Unit testing reports ](https://storage.googleapis.com/mindera-cms-media-uploads/A_Taste_Of_Unit_Testing_For_The_Client_Side3_5684598ad8/A_Taste_Of_Unit_Testing_For_The_Client_Side3_5684598ad8.jpg)

Lots of bags and packets of nuts and rice with a person's right hand at the right hand side of the image represent the featured image for a blog post about unit testing for the client-side.

A Taste Of Unit Testing For The Client-Side.

This post sums up one of our tech talks at Mindera. It serves as an introduction to both the concept of unit testing and to writing testable code.

Why?

On one hand, it’s important to have some concepts present when approaching a project to test, mainly if we come from a different context. Besides that, personally, when switching from AngularJS (which as a framework has many features that make unit testing easy) to a jQuery-based project, I came into some obstacles while beginning to unit test it. So, in this presentation, I also tried to document it.

So, unit tests, they’re cool, right?

Advantages of unit tests

  • They can serve as a ‘code’ documentation of your running component (in case of a lack of documentation)
  • They give you more confidence on the code you’re pushing to release:
  • Ensure we’re warned if new features/refactors break the expected behaviour
  • They force you to write more organised (and testable) code, or else the unit testing process is painful
  • They make your source code clearer — sometimes you find out that a certain logic is invalid/useless because you just can’t test it

And…

JavaScript is a dynamically typed language, we have no help from the compiler — easier to break if something changes (or if we code it wrong)

// Cannot read property 'length' of undefined

// undefined is not a function

Obstacles

It takes time, and we got features to deliver — we have to make space (e.g. give adequate points to US’) on the sprint grooming

It can involve some refactoring when beginning to test an already existing codebase — start as soon as possible

As the name implies, test individually each app component

This means that when testing component X, each interaction with components Y and Z will be mocked. This is because:

  • We’re not testing the other components behaviour — that belongs on Y and Z unit tests
  • We have control over our tests scenarios (component X state)

As a general rule

Test the returned value of ‘public methods’ and the current value of ‘public properties’ of the component.

Writing Testable JS code

Consider this sample app. It’s a simple page where each time the user chooses items from the ‘available list’, an ajax request is made and if it’s successful the item is removed from that list, added to the ‘my list’ section and the item counter is updated. Sort of a shopping cart concept.

Typical jQuery-ish code for this page

$(document).ready(function() { var listDiv = $('.available'), myList = $('.my-list'), itemCount = getItemCount( '.item' );

$(.items-count).html(itemCount);

listDiv.on('click',function(){
    var $clickedEL = $(this);
    $.ajax(...);
    function onSucces() {
        $clickedEL.remove();
        var $newEl;
        itemCount+=1;
        $(.items-count).html(itemCount);
        $newEl = $('<li class="item">').text($clickedEL.text());
        myList.append($newEL);
    }
});

function getItemCount( selector ){
    return $( selector ).length;
}

});

(Not testable) Why?

  • All logic is hidden inside a ready() function
  • CSS classes and jquery selectors sprayed across the snippet
  • Anonymous functions are harder to test
  • Not related with tests — code harder to reuse and customise

Testability++

function MyListComponent($context) { var compClasses = { item: 'item' }; var compSelectors = { item : '.item', component : '.my-list', itemCount : 'item-count' };

// Private
var $component = $context.find(compSelectors.component);

function initComponent() {
    updateItemCount();
}

function updateItemCount() {
    $context.find(compSelectors.itemCount).text(getItemCount());
}

function getItemCount() {
    return $context.find(compSelectors.item).length;
}

function buildItem(text) {
    return $('<li>').addClass(compClasses.item).text(text);
}

initComponent();

// Public API
this.addItem = function(text) {
    $component.append(buildItem(text));
    updateItemCount();
};

this.getItemCount = getItemCount;

}

module.exports = { create: function($context, handleAvailableListClick) { return new AvailableListComponent($context, handleAvailableListClick); } };

What happened?

By creating the MyListComponent we can:

  • Focus on what the component should do
  • Test it individually
  • Quickly pinpoint the broken logic spot on future regressions or refactors
  • Divide work when creating the tests (one dev can test just this component while another tests its sibling)

Besides that, we:

  • Increased reusability
  • Separated concerns
  • Are happier :)

We can break it down even more. On our current project, we started creating a file for ‘business’ logic and a file for ‘view’ logic (which includes visual ‘components’) for each app module.

Applying the same logic:

// Available Component Definition var compClasses = { ... }; var compSelectors = { ... };

// Private
var $component = $context.find(compSelectors.component);

// Public
this.removeItem = function($item) {
        ...
};

// delegate click handler
$component.on('click', handleAvailableListClick);

}

module.exports = { create: function($context, handleAvailableListClick) { return new AvailableListComponent($context, handleAvailableListClick); } };

// ListChooser Page definition var $page = $('#container');

var myListComponent= MyListComponent.create($page);
var availableListComponent = AvailableListComponent.create($page, handleAvailableListClick);

 
function handleAvailableListClick() { var $clickedEL = $(this); $.ajax(...); function onSuccess() { availableComponent.removItem($clickedEL); myListComponent.addItem($clickedEL.text()); }

};

// Public
this.handleAvailableListClick = handleAvailableListClick;

}

ListChooserPage.create();

What to do with this?

Now we can test the AvailableList component more easily.

At first glance:

Check if it’s being correctly initiated:

  1. If it finds the items throughout the selectors
  2. If the removeItem public method has the expected behaviour

Check if the click event delegation is correctly set up.

Testing the ListChooserPage Component:

Check if it initiates correctly:

  1. If it searches (and finds) its selectors
  2. If it instantiates its dependencies

If its click handler function is called when clicking its bound element and if it does what’s expected:

Ajax call with certain parameters and on success:

  1. Call availableComponent removeItem
  2. Call myListComponent addItem

In the end, we realise that

List component became responsible for its internal representational logic + internal jQuery actions and for wiring up its event handlers — that’s what we’re going to test Page component is in charge of the page logic, making ajax calls, setting up the event handler functions, etc… — that’s what we’re going to test

Key Concepts

Test files

Normally all the test cases related to the same part of the app are grouped into the same test file. In our previous example, we’d have three test files (one forEach() component).

Describe

A describe is a function that defines a test suite.

Spec === test

A spec contains one or more expectations that test the state of the code. A spec with all true expectations is a passing spec. A spec with one or more false expectations is a failing spec.

What does it look like?

describe("the component/behavior we're testing", function() { var myComp;

beforeEach() {
    myComp = new myComponent();
}

it("should return true when getBoolValue is called", function() {
    var fnReturnValue = myComp.getBoolValue();

    expect(fnReturnValue).toBe(true);
});

});

Matchers

HELPER FUNCTIONS USED IN EXPECTATIONS


.toEqual({yo: true}) // 'strict equal' and more general equals .toBeUndefined() .toHaveBeenCalled() .toBeTruthy() .toContain() .toThrow(e) .not.to....();

Spies

Spies are utilities for stubbing any function and tracking calls to it and all arguments.

A spy only exists in the describe or it block in which it is defined and will be removed after each spec.

What they look like

it("should call the myComp method", function() { spyOn(myComp, 'getBoolValue'); myComp.methodThatAlsoCallsGetBoolValue();

expect(myComp.getBoolValue).toHaveBeenCalled();

});

Fixtures

On AngularJS, testing is given straight out-of-the-box, the framework itself can detect templates used in directives, ngMock module can inject and mock dependencies.

An initial approach is well documented here.

On jQuery apps, the dev is more responsible for organizing the code so that it’s testable.

Also, for testing components with DOM logic, it’s necessary to inject HTML content into the tests so that jQuery has something to run against — fixtures.

Tools of the Trade

  • Karma — ‘Spectacular’ Test Runner for Javascript

Test runners:

  • Mocha — test framework for JS, normally used with:
  • Chai — assertion library (stuff lik assert, should and expect)
  • Sinon — test spies, stubs and mocks
  • Jasmine — simpler solution (although less powerfull) gives you a behaviour-driven development testing framework + expect, spies, etc… in one package
  • Karma-Browserify — Karma plugin for testing our browserifiy code (used in this demo)
  • Jasmine-jQuery — set of matchers and fixture loaders for jquery

Reporters

  • Karma Coverage — gives statement, line, function and branch coverage

and more…

Let’s do some testing, then!


Testing ListChooserPage Component

Remember what to test.

describe('ListChooserPage', function () { var ListChooserPage = require('../../components/ListChooserPageComponent'); var listChooser; (...)

beforeEach(function () { loadFixtures('listChooserPage.html'); myList = jasmine.createSpyObj('myList', ['addItem']); availableList = jasmine.createSpyObj('availableList', ['removeItem']); });

it('should initiate correctly', function () { // should check if the ListChooserPage instantiates its dependencies // set up spies spyOn(AvailableComponent, 'create'); spyOn(MyListComponent, 'create'); var $container = $('#container');

listChooser = ListChooserPage.create();

expect(MyListComponent.create).toHaveBeenCalled();
expect(AvailableComponent.create)
    .toHaveBeenCalledWith($container, listChooser.handleAvailableListClick);
  
});

it('should call endpoint X and on success removeItem from available and addItem do myList', function () { spyOn(MyListComponent, 'create').and.callFake(function() { return myList; }); spyOn(AvailableComponent, 'create').and.callFake(function() { return availableList; }); spyOn($, 'ajax').and.callFake(function() { var d = $.Deferred(); d.resolve({}); return d.promise(); }); var $container = $('#container');

listChooser = ListChooserPage.create();
listChooser.handleAvailableListClick();

expect(myList.addItem).toHaveBeenCalled();
expect(availableList.removeItem).toHaveBeenCalled();
  
});

it('should call endpoint X and if error should not call any other fn', function () { spyOn(MyListComponent, 'create').and.callFake(function() { return myList; }); spyOn(AvailableComponent, 'create').and.callFake(function() { return availableList; }); spyOn($, 'ajax').and.callFake(function() { var d = $.Deferred(); d.reject(); return d.promise(); }); var $container = $('#container');

  listChooser = ListChooserPage.create();
  listChooser.handleAvailableListClick();

  expect(myList.addItem).not.toHaveBeenCalled();
  expect(availableList.removeItem).not.toHaveBeenCalled();
  
});

}); <br

Testing AvailableList component

Remember what to test.

describe('AvailableComponent', function () { var availableList;

beforeEach(function () {
    loadFixtures('availableListFixture.html');
});

it('should initiate correctly', function () {
    // set up spies
    var $context = $('#jasmine-fixtures');
    spyOn($context, 'find').and.callThrough();
    spyOn(AvailableListComponent, 'create').and.callThrough();

    availableList = AvailableListComponent.create($context);
    expect($context.find).toHaveBeenCalledWith('.available');
});

it('should call the handleAvailableListClick callback', function () {
    // set up spies
    var handleAvailableListClickSpy = jasmine.createSpy('callback');
    var $context = $('#jasmine-fixtures');
    var $available = $('.available');

    availableList =  AvailableListComponent.create($context, handleAvailableListClickSpy);
    $available.find('li').trigger('click');
    expect(handleAvailableListClickSpy).toHaveBeenCalled();
});

});

Keep in mind that we’re doing some basic, first approach testing.

  • To complete the job, the dev should consider covering some possible error cases, like:
  • Passing invalid arguments into constructors
  • Changing the response from the ajax response to an unexpected one Etc…

It’s always important to find a balance between what to test and where. This depends on a lot of factors, such as if it’s a more critical part of the application, if it impacts others modules, if it breaks the UX — you name it.

Typical setup (for this demo project)

Entry point: karma.conf.js

THE MAIN CONFIGURATIONS ARE THE FOLLOWING:

// list of files / patterns to load in the browser files: [ 'node_modules/jasmine-jquery/lib/jasmine-jquery.js', 'test/fixtures/.html', 'test//*-spec.js' ],

// frameworks to use frameworks: ['jasmine-jquery','jasmine', 'browserify'],

// preprocess matching files before serving them to the browser preprocessors: { 'test//*.js': [ 'browserify' ] },

plugins: [ 'karma-phantomjs-launcher', 'karma-chrome-launcher', 'karma-jasmine-jquery', 'karma-jasmine', 'karma-browserify' ],

RUNNING TESTS, THE SIMPLE WAY

// on package.json ... "scripts": { "test": "./node_modules/.bin/karma start karma.conf.js" },

// on terminal npm test

But you can use whatever automation tool you prefer, it’s very flexible

Then you can get these cool reports (with hopefully a lot of green)

Unit testing reports

Final Thoughts

In the end, not all is golden. There are still some obstacles that we need to be prepared for and we must constantly try to come up with clever ways to overcome them:

  • Having visual components helps a lot in separating the code for representational logic and business logic, however when testing the visual components it’s important to use the fixtures with caution and thought, as they can become easily outdated
  • We have to approach this with ease and not go aiming at 100% coverage (which normally doesn’t mean much). Just find a balance on what to test, be smart while designing the test cases, and be on the lookout for critical paths that need more attention
  • Some people just turn every private method to public so they can test those functions more easily. I’m not 100% on that train, personally:

I prefer to read the code and evaluate what’s accessible from the outside, either using ‘getters’ to assert over the components state or checking if all the dependencies were called with the correct parameters

Of course, there are different cases and if it’s really hard to test a block that we think is essential then we can do it. Just try not to make a habit of it

Feel the need to start experimenting with unit tests?

See you soon.