Unit Testing JavaScript Applications

Preview:

DESCRIPTION

An in depth look at mocha and sinon. Slides show how to use both to write unit tests and mock objects for your JavaScript application

Citation preview

Mocha First StepsInstalling and running tests

Agenda

• JS Unit Testing

• A first Mocha test

• Running tests with Karma

• IDE integration

Getting Ready To Test

• JS Unit tests (try) make sure our JS code works well

Project Tree

index.html   - src   - main.js   - buttons.js   - player.js   - style   - master.css   - home.css  

Project Treeindex.html test.html   - src   - main.js   - buttons.js   - player.js   - style   - master.css   - home.css   - spec   - test_button.js   - test_player.js

What Mocha Isn’t

• No UI / CSS testing

• No server testing

Testing How

Testing Libraries

• Let’s try to write test program for Array

• Verify indexOf(...) actually works

Array#indexof

var arr1 = [10, 20, 30, 40];   if ( arr1.indexOf(20) === 1 ) {   console.log('success!'); } else {   console.log('error'); }

What Went Wrong

• Hard to debug

• Hard to run automatically

We Need …

We Need …

Testing Libraries

• A testing library tells you how to structure your testing code

• We’ll use mochahttp://visionmedia.github.io/mocha/

Hello Mochavar assert = chai.assert; var array = [10,20,30,40];   describe('Array', function() { !  describe('#indexOf()', function() { !    it('should return -1 when the value is not present', function() {             assert.equal(array.indexOf(7), -1);     } );   }); });

Hello Mocha

• describe() defines a block

• it() defines functionality

Assertions

• Uses a separate assertions library

• I went with Chai

• http://chaijs.com/

Running Our Test: Karma

Meet Karma

• A test runner for JS

• Integrates with many IDEs

• Integrates with CI servers

• http://karma-runner.github.io/0.10/index.html

Karma Architecture

Karma Server

Karma Getting Started

# run just once to install npm install karma -g   # create a project directory mkdir myproject cd myproject   # create karma configuration file karma init

Karma Config

• Just a JavaScript file

• keys determine how test should run

Karma Config

• files is a list of JS files to include in the test

• Can use wildcards

Karma Config

• browsers is a list of supported browsers

Running Tests

# start a karma server karma start   # execute tests karma run

IDE Integration

What We Learned

• Mocha is a JS library that helps us write unit tests

• Karma is a JS library that helps us run them

Q & A

Advanced MochaHow to write awesome tests

Agenda

• Flow control: before, after, beforeEach, afterEach

• Writing async tests

• Fixtures and DOM testing

Let’s Flowdescribe('Test 1', function() {   it('should do X', function() {     var p1 = new Player('bob');     var p2 = new Player('John');     var game = new GameEngine(p1, p2);       // test stuff with game   });     it('should do Y', function() {     var p1 = new Player('bob');     var p2 = new Player('John');     var game = new GameEngine(p1, p2);       // test stuff with game   }); });

Let’s Flowdescribe('Test 1', function() {   it('should do X', function() {     var p1 = new Player('bob');     var p2 = new Player('John');     var game = new GameEngine(p1, p2);       // test stuff with game   });     it('should do Y', function() {     var p1 = new Player('bob');     var p2 = new Player('John');     var game = new GameEngine(p1, p2);       // test stuff with game   }); });

Same code...

A Better Scheme• beforeEach() runs

before each test

• also has:

• afterEach() for cleanups

• before() and after() run once in the suite

describe('Test 1', function() {!  var game;! !  beforeEach(function() {!    var p1 = new Player('bob');!    var p2 = new Player('John');!    game = new GameEngine(p1, p2);!  });! !  it('should do X', function() {!    // test stuff with game!  });!! !  it('should do Y', function() {!    // test stuff with game!  });!});

Async Testing

Async Theory

var x = 10

test x x has the right value testing here is OK

Async Theory

$.get(...)

test result Can’t test now, result not yet ready

Async Theory

• Async calls take callbacks

• We should tell mocha to wait

Async Code

describe('Test 1', function() {     it('should do wait', function(done) {     setTimeout(function() {       // now we can test result       assert(true);       done();     }, 1000);   });   });

Async Code

describe('Test 1', function() {     it('should do wait', function(done) {     setTimeout(function() {       // now we can test result       assert(true);       done();     }, 1000);   });   });

Taking a function argument tells mocha the test will only end after it’s called

Async Code

describe('Test 1', function() {     it('should do wait', function(done) {     setTimeout(function() {       // now we can test result       assert(true);       done();     }, 1000);   });   });

Calling the callback ends the test

Async Notes

• Always call done() or your test will fail on timeout

• Default timeout is 2 seconds

Controlling Timeouts

describe('Test 1', function() {   // set suite specific timeout   this.timeout(500);     it('should do wait', function(done) {     // test specific timeout     this.timeout(2000);       }); });

Same goes for Ajax

describe('Test 1', function() {   // set suite specific timeout   this.timeout(5000);     it('should get user photo', function(done) {     $.get('profile.png', function(data) {       // run tests on data       done();     });   }); });

DOM Testing

Theory

body

“Real” HTML “Test” HTML

body

h1

div div

img

Theory

body

“Real” HTML “Test” HTML

body

h1

div div

img img

Theory

$('img.thumbnail').css({ width: 200, height: 200 });

<img class="thumbmail" src="home.png" />

fixture.html

images.js

Using Fixtures

before(function() { fixture_el = document.createElement('div'); fixture_el.id = "fixture"; ! document.body.appendChild(fixture_el); }); !beforeEach(function() { fixture_el.innerHTML = window.__html__["fixture.html"]; }); !

Almost Ready

• HTML files are not served by default

• We need to tell karma to serve it

Serving HTMLs• Modify files section to include the last

(HTML) pattern

// list of files / patterns to load in the browser files: [ 'lib/**/*.js', 'plugins/**/*.js', 'test/fixtures/*.html', 'spec/*.js' ],

Testing a jQuery Plugin

it('should change the header text lowercase', function() { $('.truncate').succinct({ size: 100 }); ! var result = $('.truncate').text(); assert.equal( result.length , 100 ); });

Fixtures & DOM

• Define DOM fragments in HTML files

• Load from test suite

• Test and clean up

Spying With SinonStubs, Spies and Mock Objects explained

Agenda• Reasons to mock

• Vanilla mocking

• How sinon can help

• Stubs and Spies

• Faking timers

• Faking the server

Reasons To Mock

Reasons To Mock

PersonalData$.ajax setTimeout

Reasons To Mock

PersonalData$.ajax setTimeout

Reasons To Mock

• PersonalData object can save data to server

• If saving failed, it retries 3 times

Reasons To Mock

• Both server and clock are external

• We prefer to test in isolation

What We Can Do

• Provide our own $.ajax, that won’t go to the server

• Provide our own setTimeout that won’t wait for the time to pass

What We Can Do

• Lab: Given the class here https://gist.github.com/ynonp/6667146

• Write a test case to verify sendData actually retried 3 times

What We Can Do

• Solution: https://gist.github.com/ynonp/6667284

Mocking Notes

• Solution is far from perfect.

• After the test our “fake” methods remain

• Writing “fake” methods was not trivial

This Calls for Sinon

About Sinon

• A JS mocking library

• Helps create fake objects

• Helps tracking them

About Sinon

• Homepage: http://sinonjs.org/

• Google Group: http://groups.google.com/group/sinonjs

• IRC Channel: #sinon.js on freenode

Solving with Sinon

• Here’s how sinon might help us with the previous task

• Code: https://gist.github.com/ynonp/6667378

Solution Notes

• Sinon’s fake timer was easier to use than writing our own

• Now we have a synchronous test (instead of async)

Let’s Talk About Sinon

Spies

• A spy is a function that provides the test code with info about how it was used

Spies Demodescribe('Sinon', function() { describe('spies', function() { ! it('should keep count', function() { ! var s = sinon.spy(); s(); assert.isTrue(s.calledOnce); ! s(); assert.isTrue(s.calledTwice); ! s(); assert.equal(s.callCount, 3); ! }); }); });

Spy On Existing Funcsdescribe('Sinon', function() { describe('spies', function() { ! it('should keep count', function() { var p = new PersonalData(); var spy = sinon.spy(p, 'sendData'); ! p.sendData(); ! assert.isTrue( spy.calledOnce ); }); }); });

Spies Notes

• Full API: http://sinonjs.org/docs/#spies

• Tip: Use as callbacks

Spy + Action = Stub

Stub Demo

• Let’s fix our starting example

• We’ll replace $.ajax with a stub

• That stub always fails

Stub Demovar stub = sinon.stub(jQuery, 'ajax').yieldsTo('error'); !describe('Data', function() { describe('#sendData()', function() { ! it('should retry 3 times before quitting', function() { var p = new PersonalData(); p.sendData(); assert.equal(stub.callCount, 1); }); }); });

What Can Stubs Do

var callback = sinon.stub(); !callback.withArgs(42).returns(1); !callback.withArgs(1).throws("TypeError"); !

Stubs API• Full Stubs API docs:

http://sinonjs.org/docs/#stubs

• Main actions:

• return stuff

• throw stuff

• call stuff

Spies Lab

• Given code here: https://gist.github.com/ynonp/7101081

• Fill in the blanks to make the tests pass

Fake Timers

• Use sinon.useFakeTimers() to create a fake timer

• Use clock.restore() to clear fake timers

Fake Timers

• Use tick(...) to advance

• Affected methods:

• setTimeout, setInterval, clearTimeout, clearInterval

• Date constructor

Fake Servers

• Testing client/server communication is hard

• Use fake servers to simplify it

Fake Servers

PersonalData$.ajax

Fake Servers

PersonalData$.ajax

Fake

Let’s write a test for the following class

1. function Person(id) { 2.   var self = this; 3.   4.   self.load = function() { 5.     var url = '/users/' + id; 6.   7.     $.get('/users/' + id, function(info) { 8.       self.name = info.name; 9.       self.favorite_color = info.favorite_color; 10.     }); 11.   }; 12. }

Testing Plan

• Set-up a fake server

• Create a new Person

• call load()

• verify fields data

Setting Up The Server

1. var server = sinon.fakeServer.create(); 2.   3. var headers  = {"Content-Type" : "application/json"}; 4. var response = JSON.stringify( 5.                 {"name" : "joe", "favorite_color": "blue" }); 6.   7. server.respondWith("GET", "/users/7", 8.                    [200, headers, response]); 9. // now requesting /user/info.php returns joe's info as a JSON

Loading a Person

1. var p = new Person(7); 2. // sends a request 3. p.load(); 4.   5. // now we have 1 pending request, let's fake the response 6. server.respond();

Verifying the Data

1. // finally, verify data 2. expect(p.name).to.eq('joe'); 3. expect(p.favorite_color).to.eq('blue'); 4.   5. // and restore AJAX behavior 6. server.restore();

Fake Server

• Use respondWith() to set up routes

• Use respond() to send the response

Fake Server• Regexps are also supported, so this works:

1. server.respondWith(/\/todo-items\/(\d+)/, function (xhr, id) { 2.     xhr.respond( 3.       200, 4.       { "Content-Type": "application/json" }, 5.       '[{ "id": ' + id + ' }]'); 6. });

Fake Server

• For fine grained control, consider fake XMLHttpRequest

• http://sinonjs.org/docs/#server

Wrapping Up

Wrapping Up

• Unit tests work best in isolation

• Sinon will help you isolate units, by faking their dependencies

Wrapping Up

• Write many tests

• Each test verifies a small chunk of code

• Don’t test everything

Online Resources• Chai:

http://chaijs.com/

• Mocha: http://visionmedia.github.io/mocha/

• Sinon: http://sinonjs.org/

• Karma (test runner): http://karma-runner.github.io/0.10/index.html

Thanks For Listening

• Ynon Perek

• http://ynonperek.com

• ynon@ynonperek.com