A Taste of Unit Testing for the Client-side
This post sums up the talk I did at Mindera, in one of our TechTalks. It comes as an introduction both to 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 begining to unit test it, so in this presentation I also tried to document it.
So, Unit tests - they’re cool, right?
Advantages
- unit tests 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 that we’re warned if new features/refactors break the expected behavior
- they enforce you to write more organized code (and testable) code, or else the unit testing proccess 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 - more easier to break if something changes (or if we code it wrong)
// Cannot read property 'length' of undefined // undefined is not a function
anyone?
Obstacles
- it takes time, and we got features to deliver - we have to make space (i.e. give adequate points to US’s) on the sprint grooming
-
it can involve some refactoring when begining 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 the component X, each interaction with components Y and Z will be mocked, because:
- we’re not testing the other components behavior, that belongs on Y and Z unit tests
- we have control over our tests scenarios (component X state)
Rule of thumb:
Test the returned value of ‘public methods’ and 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 accross the snippet
- anonymous functions are harder to test
- (not related with tests) - code harder to reuse and customize
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
function AvailableComponent($context, handleAvailableListClick) {
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
function ListChooserPage() {
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:
- if it finds the items throught the selectors
- if the
removeItem
public method has the expected behavior
- check if the click event delegation is correctly set up
Testing the ListChooserPage Component:
- check if it initiates correctly
- if searches (and finds) its selectors
- if 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:
- call availableComponent
removeItem
- call myListComponent
addItem
In the end we realize 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 3 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 it looks 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.
how 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
- 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, as listed here.
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();
});
});
Testing AvailableList Component
Remember what to test, as listed here.
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
- change the response from the ajax response to an unexpected one
- etc
It’s always important to find a balance on what to test and where, and that depends on a lot of factors (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
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)
Final Thoughts
At the end, not all is golden, there are still some obstacles that we need to be prepared for, and constantly try to come up with clever ways to overcome them:
- having visual components helps a lot on 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 which need more attention
- some people just turn every private method to public so they can test those function 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 which we think is essential, we can do it, just try not to make a habit of it
Reference links
- Introduction To JavaScript Unit Testing
- Writing Testable JavaScript
- Unit Testing JavaScript Using Jasmine
Feel the need to start experimenting with unit tests?
git checkout
this
See you soon,
A. Capelo
Tweet