Workshop 5: JavaScript testing

Post on 14-Jan-2017

387 views 1 download

transcript

Front End Workshops

VI.JavaScript testing. Client vs. Server testing. Test Driven

Development

Raúl Delgado Astudillordelgado@visual-engin.com

Mario García Martínmgarcia@visual-engin.com

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