workshop 5: javascript testing
TRANSCRIPT
Front End Workshops
VI.JavaScript testing. Client vs. Server testing. Test Driven
Development
Raúl Delgado [email protected]
Mario García Martí[email protected]
JavaScript testing
“Testing is an infinite process of comparing the invisible to the ambiguous in order to avoid the unthinkable happening to the
anonymous.”— James Bach
What is a test?
Type some code
Open and load the browser
Prove functionality
A test is (simply) the validation of an expectation.
Manual testing...
...isNOT
enough!
Can we do better?
Manual testing is...
Time consuming
Error prone
Irreproducible
(Nearly) Impossible if we want to test a wide set of browsers and platforms
YES!!
Automated testing
Tests should be
Fast to run
Easy to understand
Isolated
Not reliant on an Internet connection
Benefits and pitfalls of testing
Regression testing
Refactoring
Cross-browser testing
Good documentation
Helps us write cleaner interfaces (testable code)
Writing good tests can be challenging
More information in...
● Test-Driven JavaScript Development, by Christian Johansen
● https://en.wikipedia.org/wiki/Software_testing
Client testing
“A passing test doesn't mean no problem. It means no problem observed. This time. With these inputs. So far. On my machine.”
— Michael Bolton
Frameworks
Jasmine — Scaffolding
describe("A suite with setup and tear-down", function() {var foo;
beforeAll(function() {});
afterAll(function() {});
beforeEach(function() {foo = 1;
});
afterEach(function() {foo = 0;
});
it("can contain specs with one or more expectations", function() {expect(foo).toBe(1);
expect(true).toBe(true);});
});
Matchersexpect(3).toBe(3); // Compares with ===expect({a: 3}).toEqual({a: 3}); // For comparison of objectsexpect('barely').toMatch(/bar/); // For regular expressionsexpect(null).toBeDefined(); // Compares against undefinedexpect(undefined).toBeUndefined(); // Compares against undefinedexpect(null).toBeNull(); // Compares against nullexpect('hello').toBeTruthy(); // For boolean casting testingexpect('').toBeFalsy(); // For boolean casting testingexpect(['bar', 'foo']).toContain('bar'); // For finding an item in an Arrayexpect(2).toBeLessThan(3); // For mathematical comparisonsexpect(3).toBeGreaterThan(2); // For mathematical comparisonsexpect(3.14).toBeCloseTo(3.17, 1); // For precision math comparison
// For testing if a function throws an exceptionexpect(function() { throw new Error('Error!'); }).toThrow();
// Modifier 'not'expect(false).not.toBe(true);
Spies
describe("A suite", function() {var foo, bar = null;
beforeEach(function() {foo = { setBar: function(value) { bar = value; } };spyOn(foo, 'setBar');foo.setBar(123);foo.setBar(456, 'another param');
});
it("that defines a spy out of the box", function() {expect(foo.setBar).toHaveBeenCalled(); // tracks that the spy was called
// tracks all the arguments of its callsexpect(foo.setBar).toHaveBeenCalledWith(123);expect(foo.setBar).toHaveBeenCalledWith(456, 'another param');
expect(bar).toBeNull(); // stops all execution on a function});
});
Spies — and.callthrough
describe("A suite", function() {var foo, bar = null;
beforeEach(function() {foo = {
setBar: function(value) { bar = value; }};
spyOn(foo, 'setBar').and.callThrough();foo.setBar(123);
});
it("that defines a spy configured to call through", function() {expect(foo.setBar).toHaveBeenCalled(); // tracks that the spy was called
expect(bar).toEqual(123); // the spied function has been called});
});
describe("A suite", function() {var foo, bar = null;
beforeEach(function() {foo = {
getBar: function() { return bar; }};
spyOn(foo, 'getBar').and.returnValue(745);});
it("that defines a spy configured to fake a return value", function() {expect(foo.getBar()).toBe(745); // when called returns the requested value
expect(bar).toBeNull(); // should not affect the variable});
});
Spies — and.returnValue
describe("A suite", function() {var foo, bar = null;
beforeEach(function() {foo = {
setBar: function(value) { bar = value; }};
spyOn(foo, 'setBar').and.callFake(function() {console.log('hello');
});foo.setBar(); // logs hello in the console.
});
it("that defines a spy configured with an alternate implementation", function() {expect(foo.setBar).toHaveBeenCalled(); // tracks that the spy was called
expect(bar).toBeNull(); // should not affect the variable});
});
Spies — and.callFake
Spies — createSpydescribe("A suite", function() {
var spy;
beforeAll(function() {$(window).on('resize', function() { $(window).trigger('myEvent'); });
});
afterAll(function() {$(window).off('resize');});
beforeEach(function() {spy = jasmine.createSpy();
});
it("that defines a spy created manually", function() {$(window).on('myEvent', spy);$(window).trigger('resize');expect(spy).toHaveBeenCalled(); // tracks that the spy was called
});});
Spies — Other tracking properties (I)describe("A spy", function() {
var foo, bar = null;
beforeEach(function() {foo = { setBar: function(value) { bar = value; } };spyOn(foo, 'setBar');foo.setBar(123);foo.setBar(456, 'baz');
});
it("has a rich set of tracking properties", function() {expect(foo.setBar.calls.count()).toEqual(2); // tracks the number of calls// tracks the args of each callexpect(foo.setBar.calls.argsFor(0)).toEqual([123]);expect(foo.setBar.calls.argsFor(1)).toEqual([456, 'baz']);// has shortcuts to the first and most recent callexpect(foo.setBar.calls.first().args).toEqual([123]);
expect(foo.setBar.calls.mostRecent().args).toEqual([456, 'baz']);});
});
Spies — Other tracking properties (II)describe("A spy", function() {
var foo, bar = null;
beforeEach(function() {foo = { setBar: function(value) { bar = value; } };spyOn(foo, 'setBar');foo.setBar(123);foo.setBar(456, 'baz');
});
it("has a rich set of tracking properties", function() {// tracks the context and return values
expect(foo.setBar.calls.first().object).toEqual(foo);expect(foo.setBar.calls.first().returnValue).toBeUndefined();
// can be resetfoo.setBar.calls.reset();expect(foo.setBar.calls.count()).toBe(0);
});});
Asynchronous support
describe("Asynchronous specs", function() {var value;
beforeEach(function(done) {setTimeout(function() {
value = 0;done();
}, 100);});
it("should support async execution of preparation and expectations", function(done) {expect(value).toBe(0);done();
});});
Clock
describe("Manually ticking the Jasmine Clock", function() {var timerCallback;
beforeEach(function() {timerCallback = jasmine.createSpy();jasmine.clock().install();
});
afterEach(function() {jasmine.clock().uninstall();
});
it("causes a timeout to be called synchronously", function() {setTimeout(timerCallback, 100);
expect(timerCallback).not.toHaveBeenCalled();jasmine.clock().tick(101);expect(timerCallback).toHaveBeenCalled();
});});
Clock — Mocking the date
describe("Mocking the Date object", function() {beforeEach(function() {jasmine.clock().install();
});
afterEach(function() {jasmine.clock().uninstall();
});
it("mocks the Date object and sets it to a given time", function() {var baseTime = new Date(2013, 9, 23);
jasmine.clock().mockDate(baseTime);
jasmine.clock().tick(50);expect(new Date().getTime()).toEqual(baseTime.getTime() + 50);
});});
Sinon — Spies and Stubs
var spy = sinon.spy();
sinon.spy($, 'ajax');
$.ajax.restore();
sinon.stub($, 'ajax');
$.ajax.restore();
sinon.stub($, 'ajax', function(options) {console.log(options.url);
});$.ajax.restore();
Sinon — Fake timerdescribe("Manually ticking the Clock", function() {
var clock, timerCallback;
beforeEach(function() {timerCallback = sinon.spy();clock = sinon.useFakeTimers();
});
afterEach(function() {clock.restore();
});
it("causes a timeout to be called synchronously", function() {setTimeout(timerCallback, 100);
expect(timerCallback.callCount).toBe(0);clock.tick(101);expect(timerCallback.callCount).toBe(1);expect(new Date().getTime()).toBe(101);
});});
Sinon — Fake server
describe("A suite with a sinon fakeServer", function() {var server;
beforeEach(function() {server = sinon.fakeServer.create();server.autoRespond = true;server.respondWith(function(xhr) {
xhr.respond(200, {'Content-Type':'application/json'}, JSON.stringify({'msg': 'msg'}));});server.xhr.useFilters = true;server.xhr.addFilter(function(method, url) {
return !!url.match(/fixtures|css/); // If returns true the request will not be faked.});
});
afterEach(function() {server.restore();
});});
More information in...
● https://en.wikipedia.org/wiki/List_of_unit_testing_frameworks#JavaScript
● http://stackoverflow.com/questions/300855/javascript-unit-test-tools-for-tdd
● http://jasmine.github.io/
● http://sinonjs.org/
Test Driven Development
“The best TDD can do is assure that the code does what the programmer thinks it should do. That is pretty good by the way.”
— James Grenning
The cycle of TDD
Write a test
Run tests. Watch the new test fail
Make the test pass
Refactor to remove duplication
Benefits of TDD
Produces code that works
Honors the Single Responsibility Principle
Forces conscious development
Productivity boost
More information in...
● Test-Driven Development By Example, by Kent Beck.
Jasmine Disabling specs
xdescribe("A disabled suite", function() {it("where the specs will not be executed", function() {
expect(2).toEqual(1);});
});
describe("A suite", function() {xit("with a disabled spec declared with 'xit'", function() {
expect(true).toBe(false);});
it.only("with a spec that will be executed", function() {expect(1).toBe(1);
});
it("with another spec that will not be executed", function() {expect(1).toBe(1);
});});
Asynchronous support
describe("long asynchronous specs", function() {beforeEach(function(done) {
done();}, 1000);
afterEach(function(done) {done();
}, 1000);
it("takes a long time", function(done) {setTimeout(done, 9000);
}, 10000);});
Asynchronous support. Jasmine 1.3
describe("Asynchronous specs", function() {var value, flag;
it("should support async execution of test preparation and expectations", function() {flag = false;value = 0;setTimeout(function() {
flag = true;}, 500);waitsFor(function() {
value++;return flag;
}, "The Value should be incremented", 750);runs(function() {
expect(value).toBeGreaterThan(0);});
});});
jasmine.Clock v.1.3
it('description', function() {jasmine.Clock.useMock();
setTimeout(function() {console.log('print something');
}, 200);
jasmine.Clock.tick(190);});
it('description', function() {jasmine.Clock.useMock();
jasmine.Clock.tick(190);});
Karma
npm install karma --save-devnpm install karma-jasmine karma-chrome-launcher karma-phantomjs-launcher --save-devnpm install karma-coverage --save-devnpm install -g karma-cli
Installation
Configurationkarma init karma.conf.js npm install grunt-karma --save-dev
grunt.loadNpmTasks('grunt-karma');karma: {
unit: {configFile: 'karma.conf.js'
}}
Grunt task
Karma configuration
The files array determines which files are included in the browser and which files are watched and served by Karma.
Each pattern is either a simple string or an object with four properties:
pattern String, no default value. The pattern to use for matching. This property is mandatory.
watched Boolean (true). If autoWatch is true all files that have set watched to true will be watched for changes.
includedBoolean (true). Should the files be included in the browser using <script> tag? Use false if you want to load them manually, eg. using Require.js.
served Boolean (true). Should the files be served by Karma's webserver?
THANKS FOR YOUR ATTENTION
Leave your questions on the comments section