Testing Javascript Applications
by Dzmitry Herasimov
"Any feature without a test doesn’t exist"
Steve Loughran HP Laboratories
¿Por qué?
¡Porque!
- refactor the code;
- organize & modularize;
- provide secondary documentation;
- prevent defects;
- collaborate;
Tests help you to:
Testing + CI = ❤
-
fail builds
-
statistics & reporting
-
Github is integration paradise

Tests
Some boring theory
Test types
Unit
Component/Snapshot
Integration Tests
e2e Tests
Security tests
Accessibility tests
The Test Pyramid
Unit tests
Integration tests
UI tests
More
integration
More
isolation
Slow
Fast
- Setup: Put the Unit Under Test (UUT) or the overall test system in the state needed to run the test.
- Execution: Trigger/drive the UUT to perform the target behavior and capture all output, such as return values and output parameters.
- Validation: Ensure the results of the test are correct.
- Cleanup: Restore the UUT or the overall test system to the pre-test state.
Structure
http://en.wikipedia.org/wiki/Test-driven_development#Test_structure
Tests F.I.R.S.T.
NOT! just First
F - Fast

- Test, including setup and tear down, should execute really fast (ms) .
- A developer should not hesitate to run the tests as they are slow.
50ms
51ms
100ms
I - Isolated/Independent
- Arrange: The data used in a test should not depend on the environment. All the data needed for a test should be arranged as part of the test.
- Act: Invoke the actual method under test.
- Assert: A test method should test for a single logical outcome.
- Avoid doing asserts in the Arrange part, let it throw exceptions and your test will still fail.
- No order-of-run dependency. They should pass or fail the same way in suite or when run individually.
- Do not do any more actions after the assert statement(s), preferably single logical assert.

R - Repetable
- Deterministic results - should yield the same results every time and at every location where they run.
- No dependency on date/time or random functions output.
- Each test should setup or arrange it's own data.
- Use Data Helper classes that can setup this data for re-usability.

Self-Validating
- No manual inspection required to check whether the test has passed or failed.

Thorough and Timely
- Should cover every use case scenario and NOT just aim for 100% coverage.
- Should try to aim for Test Driven Development (TDD) so that code does not need re-factoring later.

Test driven development
TDD
now really first
Tests first

Red
Green
Refactor
Red: Write failing tests

Red
Green
Refactor
Green: Make it pass

Red
Green
Refactor
Refactor: Eliminate redundancy

Behaviour Driven Development
BDD
Deliver software that matters
BDD
-
Where to start in the process?
-
What to test and what not to test?
-
How much to test in one go?
-
What to call the tests?
-
How to understand why a test fails?
"From the heavens to the depths"
Behavioral specifications
User story:
As a [role] I want [feature] so that [benefit].
Acceptance criteria:
Given [initial context].
When [event occurs].
Then [ensure some outcomes].
Example
Feature: Addition
In order to avoid silly mistakes
As a math idiot
I want to be told the sum of two number
Scenario: Add two numbers
Given I have entered 50 into the calculator
And I have entered 70 into the calculator
When I press add
Then the result should be 120 on the screen
Techniques
Dummies

- Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists.
Stubs

-
Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.
Mocks

- A mock is something that as part of your test you have to setup with your expectations. A mock is not setup in a predetermined way so you have code that does it in your test. Mocks in a way are determined at runtime since the code that sets the expectations has to run before they do anything.
Spies
Fixtures

...finally... bothered!
Unit Tests
Today sponsors
-
Jasmine
-
Mocha ['mɔkə]
-
Chai
-
Sinon
Not suits
Suites & Specs
// Suite
describe("<unit or class name here>", function() {
// Some variables and hooks* for test suite
describe("#<method or test item name here>", function() {
// Spec (your test)
it("<behavior and result here>", function() {
/*
Initalization
Actions
Assertion
*/
});
});
});
Common test
describe('Array', function(){
describe('#indexOf()', function(){
it('should return -1 when the value is not present', function(){
[1,2,3].indexOf(5).should.equal(-1);
})
})
})
What? Where? How many?
Assertions
Classic
Assert
var assert = chai.assert;
assert.typeOf(foo, 'string');
assert.equal(foo, 'bar');
assert.lengthOf(foo, 3)
assert.property(tea, 'flavors');
assert.lengthOf(tea.flavors, 3);
BDD style
Expect
var expect = chai.expect;
expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(foo).to.have.length(3);
expect(tea).to
.have
.property('flavors')
.with.length(3);
even more BDD
Should
chai.should();
foo.should.be.a('string');
foo.should.equal('bar');
foo.should.have.length(3);
tea.should.have.property('flavors')
.with.length(3);
not meat-hook!
Hooks
beforeEach(function(done){
db.clear(function(err){
if (err) return done(err);
db.save([tobi, loki, jane], done);
});
})
['mɔkə]
Mocha
features
Mocha
-
Supports TDD assertions and BDD should/expect
-
Reporting & CI integration
-
Browser Test Runner

Async test
describe('User', function(){
describe('#save()', function(){
it('should save without error', function(done){
var user = new User('Luna');
user.save(function(err){
if (err) throw err;
done();
});
})
})
})
Hooks: before(), after(), beforeEach(), afterEach()
Hooks
beforeEach(function(done){
db.clear(function(err){
if (err) return done(err);
db.save([tobi, loki, jane], done);
});
})
Reporters




Jasmine

What is it?

Matchers
expect(x).toEqual(y);
expect(x).toBe(y);
expect(x).toMatch(pattern);
expect(x).toBeDefined();
expect(x).toBeUndefined();
expect(x).toBeNull();
expect(x).toBeTruthy();
expect(x).toBeFalsy();
expect(x).toContain(y);
expect(x).toBeLessThan(y);
expect(x).toBeGreaterThan(y);
expect(function(){fn();}).toThrow(e);
Custom matcher
var customMatchers = {
toBeGoofy: function(util, customEqualityTesters) {
return {
compare: function(actual, expected) {
if (expected === undefined) {
expected = '';
}
var result = {};
result.pass = util.equals(actual.hyuk, "gawrsh" + expected, customEqualityTesters);
if (result.pass) {
result.message = "Expected " + actual + " not to be quite so goofy";
} else {
result.message = "Expected " + actual + " to be goofy, but it was not very goofy";
}
return result;
}
};
}
};
Spies
spyOn(obj, 'method');
expect(obj.method).toHaveBeenCalled();
expect(obj.method).toHaveBeenCalledWith('foo', 'bar')
obj.method.callCount
obj.method.mostRecentCall.args
obj.method.reset()
spyOn(obj, 'method').andCallThrough()
obj.method.argsForCall
spyOn(obj, 'method').andReturn('Pow!')
Any
describe("jasmine.any", function() {
it("matches any value", function() {
expect({}).toEqual(jasmine.any(Object));
expect(12).toEqual(jasmine.any(Number));
});
});
Clock
beforeEach(function() {
timerCallback = jasmine.createSpy("timerCallback"); //create spy
jasmine.Clock.useMock(); //use wrapper of system timer
});
it("causes a timeout to be called synchronously", function() {
setTimeout(function() {
timerCallback();
}, 100);
expect(timerCallback).not.toHaveBeenCalled();
jasmine.Clock.tick(101); //make time go
expect(timerCallback).toHaveBeenCalled();
});
Chai.JS

Chai
chai.should();
foo.should.be.a('string');
foo.should.equal('bar');
foo.should.have.lengthOf(3);
tea.should.have.property('flavors')
.with.lengthOf(3);
var expect = chai.expect;
expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(foo).to.have.lengthOf(3);
expect(tea).to.have.property('flavors')
.with.lengthOf(3);
var assert = chai.assert;
assert.typeOf(foo, 'string');
assert.equal(foo, 'bar');
assert.lengthOf(foo, 3)
assert.property(tea, 'flavors');
assert.lengthOf(tea.flavors, 3);
Question Time!
How would you test RNG?
Plugins
Chai
Sinon.js
- Standalone test spies, stubs and mocks for JavaScript.
- No dependencies, works with any unit testing framework.
Spies
it("calls original function with right this and args", function () {
var callback = sinon.spy();
var proxy = once(callback);
var obj = {};
proxy.call(obj, 1, 2, 3);
assert(callback.calledOn(obj));
assert(callback.calledWith(1, 2, 3));
});
Stubs
it("returns the value from the original function", function () {
var callback = sinon.stub().returns(42);
var proxy = once(callback);
assert.equals(proxy(), 42);
});
Ajax
function getTodos(listId, callback) {
jQuery.ajax({
url: "/todo/" + listId + "/items",
success: function (data) {
// Node-style CPS: callback(err, data)
callback(null, data);
}
});
}
Ajax
after(function () {
// When the test either fails or passes, restore the original
// jQuery ajax function (Sinon.JS also provides tools to help
// test frameworks automate clean-up like this)
jQuery.ajax.restore();
});
it("makes a GET request for todo items", function () {
sinon.stub(jQuery, "ajax");
getTodos(42, sinon.spy());
assert(jQuery.ajax.calledWithMatch({ url: "/todo/42/items" }));
});
Fake XMLHttpRequest
var xhr, requests;
before(function () {
xhr = sinon.useFakeXMLHttpRequest();
requests = [];
xhr.onCreate = function (req) { requests.push(req); };
});
after(function () {
// Like before we must clean up when tampering with globals.
xhr.restore();
});
it("makes a GET request for todo items", function () {
getTodos(42, sinon.spy());
assert.equals(requests.length, 1);
assert.match(requests[0].url, "/todo/42/items");
});
Fake server
var server;
before(function () { server = sinon.fakeServer.create(); });
after(function () { server.restore(); });
it("calls callback with deserialized data", function () {
var callback = sinon.spy();
getTodos(42, callback);
// This is part of the FakeXMLHttpRequest API
server.requests[0].respond(
200,
{ "Content-Type": "application/json" },
JSON.stringify([{ id: 1, text: "Provide examples", done: true }])
);
assert(callback.calledOnce);
});
Fake time
function throttle(callback) {
var timer;
return function () {
var args = [].slice.call(arguments);
clearTimeout(timer);
timer = setTimeout(function () {
callback.apply(this, args);
}, 100);
};
}
Fake time
var clock;
before(function () { clock = sinon.useFakeTimers(); });
after(function () { clock.restore(); });
it("calls callback after 100ms", function () {
var callback = sinon.spy();
var throttled = throttle(callback);
throttled();
clock.tick(99);
assert(callback.notCalled);
clock.tick(1);
assert(callback.calledOnce);
}
Coding time!
The End
Testing Javascript Applications
By Timon Thelure
Testing Javascript Applications
- 700