by Dzmitry Herasimov
"Any feature without a test doesn’t exist"
Steve Loughran HP Laboratories
Tests help you to:
fail builds
statistics & reporting
Github is integration paradise
Some boring theory
Unit
Component/Snapshot
Integration Tests
e2e Tests
Security tests
Accessibility tests
Unit tests
Integration tests
UI tests
More
integration
More
isolation
Slow
Fast
http://en.wikipedia.org/wiki/Test-driven_development#Test_structure
NOT! just First
50ms
51ms
100ms
Test driven development
now really first
Red: Write failing tests
Green: Make it pass
Refactor: Eliminate redundancy
Behaviour Driven Development
Deliver software that matters
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"
User story:
As a [role] I want [feature] so that [benefit].
Acceptance criteria:
Given [initial context].
When [event occurs].
Then [ensure some outcomes].
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
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.
...finally... bothered!
Jasmine
Mocha ['mɔkə]
Chai
Sinon
Not suits
// 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
*/
});
});
});
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?
Classic
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
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
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!
beforeEach(function(done){
db.clear(function(err){
if (err) return done(err);
db.save([tobi, loki, jane], done);
});
})
['mɔkə]
features
Supports TDD assertions and BDD should/expect
Reporting & CI integration
Browser Test Runner
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()
beforeEach(function(done){
db.clear(function(err){
if (err) return done(err);
db.save([tobi, loki, jane], done);
});
})
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);
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;
}
};
}
};
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!')
describe("jasmine.any", function() {
it("matches any value", function() {
expect({}).toEqual(jasmine.any(Object));
expect(12).toEqual(jasmine.any(Number));
});
});
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.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);
Plugins
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));
});
it("returns the value from the original function", function () {
var callback = sinon.stub().returns(42);
var proxy = once(callback);
assert.equals(proxy(), 42);
});
function getTodos(listId, callback) {
jQuery.ajax({
url: "/todo/" + listId + "/items",
success: function (data) {
// Node-style CPS: callback(err, data)
callback(null, data);
}
});
}
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" }));
});
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");
});
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);
});
function throttle(callback) {
var timer;
return function () {
var args = [].slice.call(arguments);
clearTimeout(timer);
timer = setTimeout(function () {
callback.apply(this, args);
}, 100);
};
}
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);
}