Transcript
Page 1: Unit Testing JavaScript Applications

Mocha First StepsInstalling and running tests

Page 2: Unit Testing JavaScript Applications

Agenda

• JS Unit Testing

• A first Mocha test

• Running tests with Karma

• IDE integration

Page 3: Unit Testing JavaScript Applications

Getting Ready To Test

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

Page 4: Unit Testing JavaScript Applications

Project Tree

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

Page 5: Unit Testing JavaScript Applications

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

Page 6: Unit Testing JavaScript Applications

What Mocha Isn’t

• No UI / CSS testing

• No server testing

Page 7: Unit Testing JavaScript Applications

Testing How

Page 8: Unit Testing JavaScript Applications

Testing Libraries

• Let’s try to write test program for Array

• Verify indexOf(...) actually works

Page 9: Unit Testing JavaScript Applications

Array#indexof

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

Page 10: Unit Testing JavaScript Applications

What Went Wrong

• Hard to debug

• Hard to run automatically

Page 11: Unit Testing JavaScript Applications

We Need …

Page 12: Unit Testing JavaScript Applications

We Need …

Page 13: Unit Testing JavaScript Applications

Testing Libraries

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

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

Page 14: Unit Testing JavaScript Applications

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);     } );   }); });

Page 15: Unit Testing JavaScript Applications

Hello Mocha

• describe() defines a block

• it() defines functionality

Page 16: Unit Testing JavaScript Applications

Assertions

• Uses a separate assertions library

• I went with Chai

• http://chaijs.com/

Page 17: Unit Testing JavaScript Applications

Running Our Test: Karma

Page 18: Unit Testing JavaScript Applications

Meet Karma

• A test runner for JS

• Integrates with many IDEs

• Integrates with CI servers

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

Page 19: Unit Testing JavaScript Applications

Karma Architecture

Karma Server

Page 20: Unit Testing JavaScript Applications

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

Page 21: Unit Testing JavaScript Applications

Karma Config

• Just a JavaScript file

• keys determine how test should run

Page 22: Unit Testing JavaScript Applications

Karma Config

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

• Can use wildcards

Page 23: Unit Testing JavaScript Applications

Karma Config

• browsers is a list of supported browsers

Page 24: Unit Testing JavaScript Applications

Running Tests

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

Page 25: Unit Testing JavaScript Applications

IDE Integration

Page 26: Unit Testing JavaScript Applications

What We Learned

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

• Karma is a JS library that helps us run them

Page 27: Unit Testing JavaScript Applications

Q & A

Page 28: Unit Testing JavaScript Applications

Advanced MochaHow to write awesome tests

Page 29: Unit Testing JavaScript Applications

Agenda

• Flow control: before, after, beforeEach, afterEach

• Writing async tests

• Fixtures and DOM testing

Page 30: Unit Testing JavaScript Applications

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   }); });

Page 31: Unit Testing JavaScript Applications

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...

Page 32: Unit Testing JavaScript Applications

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!  });!});

Page 33: Unit Testing JavaScript Applications

Async Testing

Page 34: Unit Testing JavaScript Applications

Async Theory

var x = 10

test x x has the right value testing here is OK

Page 35: Unit Testing JavaScript Applications

Async Theory

$.get(...)

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

Page 36: Unit Testing JavaScript Applications

Async Theory

• Async calls take callbacks

• We should tell mocha to wait

Page 37: Unit Testing JavaScript Applications

Async Code

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

Page 38: Unit Testing JavaScript Applications

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

Page 39: Unit Testing JavaScript Applications

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

Page 40: Unit Testing JavaScript Applications

Async Notes

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

• Default timeout is 2 seconds

Page 41: Unit Testing JavaScript Applications

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);       }); });

Page 42: Unit Testing JavaScript Applications

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();     });   }); });

Page 43: Unit Testing JavaScript Applications

DOM Testing

Page 44: Unit Testing JavaScript Applications

Theory

body

“Real” HTML “Test” HTML

body

h1

div div

img

Page 45: Unit Testing JavaScript Applications

Theory

body

“Real” HTML “Test” HTML

body

h1

div div

img img

Page 46: Unit Testing JavaScript Applications

Theory

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

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

fixture.html

images.js

Page 47: Unit Testing JavaScript Applications

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"]; }); !

Page 48: Unit Testing JavaScript Applications

Almost Ready

• HTML files are not served by default

• We need to tell karma to serve it

Page 49: Unit Testing JavaScript Applications

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' ],

Page 50: Unit Testing JavaScript Applications

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 ); });

Page 51: Unit Testing JavaScript Applications

Fixtures & DOM

• Define DOM fragments in HTML files

• Load from test suite

• Test and clean up

Page 52: Unit Testing JavaScript Applications

Spying With SinonStubs, Spies and Mock Objects explained

Page 53: Unit Testing JavaScript Applications

Agenda• Reasons to mock

• Vanilla mocking

• How sinon can help

• Stubs and Spies

• Faking timers

• Faking the server

Page 54: Unit Testing JavaScript Applications

Reasons To Mock

Page 55: Unit Testing JavaScript Applications

Reasons To Mock

PersonalData$.ajax setTimeout

Page 56: Unit Testing JavaScript Applications

Reasons To Mock

PersonalData$.ajax setTimeout

Page 57: Unit Testing JavaScript Applications

Reasons To Mock

• PersonalData object can save data to server

• If saving failed, it retries 3 times

Page 58: Unit Testing JavaScript Applications

Reasons To Mock

• Both server and clock are external

• We prefer to test in isolation

Page 59: Unit Testing JavaScript Applications

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

Page 60: Unit Testing JavaScript Applications

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

Page 61: Unit Testing JavaScript Applications

What We Can Do

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

Page 62: Unit Testing JavaScript Applications

Mocking Notes

• Solution is far from perfect.

• After the test our “fake” methods remain

• Writing “fake” methods was not trivial

Page 63: Unit Testing JavaScript Applications

This Calls for Sinon

Page 64: Unit Testing JavaScript Applications

About Sinon

• A JS mocking library

• Helps create fake objects

• Helps tracking them

Page 65: Unit Testing JavaScript Applications

About Sinon

• Homepage: http://sinonjs.org/

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

• IRC Channel: #sinon.js on freenode

Page 66: Unit Testing JavaScript Applications

Solving with Sinon

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

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

Page 67: Unit Testing JavaScript Applications

Solution Notes

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

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

Page 68: Unit Testing JavaScript Applications

Let’s Talk About Sinon

Page 69: Unit Testing JavaScript Applications

Spies

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

Page 70: Unit Testing JavaScript Applications

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); ! }); }); });

Page 71: Unit Testing JavaScript Applications

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 ); }); }); });

Page 72: Unit Testing JavaScript Applications

Spies Notes

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

• Tip: Use as callbacks

Page 73: Unit Testing JavaScript Applications

Spy + Action = Stub

Page 74: Unit Testing JavaScript Applications

Stub Demo

• Let’s fix our starting example

• We’ll replace $.ajax with a stub

• That stub always fails

Page 75: Unit Testing JavaScript Applications

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); }); }); });

Page 76: Unit Testing JavaScript Applications

What Can Stubs Do

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

Page 77: Unit Testing JavaScript Applications

Stubs API• Full Stubs API docs:

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

• Main actions:

• return stuff

• throw stuff

• call stuff

Page 78: Unit Testing JavaScript Applications

Spies Lab

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

• Fill in the blanks to make the tests pass

Page 79: Unit Testing JavaScript Applications

Fake Timers

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

• Use clock.restore() to clear fake timers

Page 80: Unit Testing JavaScript Applications

Fake Timers

• Use tick(...) to advance

• Affected methods:

• setTimeout, setInterval, clearTimeout, clearInterval

• Date constructor

Page 81: Unit Testing JavaScript Applications

Fake Servers

• Testing client/server communication is hard

• Use fake servers to simplify it

Page 82: Unit Testing JavaScript Applications

Fake Servers

PersonalData$.ajax

Page 83: Unit Testing JavaScript Applications

Fake Servers

PersonalData$.ajax

Fake

Page 84: Unit Testing JavaScript Applications

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. }

Page 85: Unit Testing JavaScript Applications

Testing Plan

• Set-up a fake server

• Create a new Person

• call load()

• verify fields data

Page 86: Unit Testing JavaScript Applications

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

Page 87: Unit Testing JavaScript Applications

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();

Page 88: Unit Testing JavaScript Applications

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();

Page 89: Unit Testing JavaScript Applications

Fake Server

• Use respondWith() to set up routes

• Use respond() to send the response

Page 90: Unit Testing JavaScript Applications

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. });

Page 91: Unit Testing JavaScript Applications

Fake Server

• For fine grained control, consider fake XMLHttpRequest

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

Page 92: Unit Testing JavaScript Applications

Wrapping Up

Page 93: Unit Testing JavaScript Applications

Wrapping Up

• Unit tests work best in isolation

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

Page 94: Unit Testing JavaScript Applications

Wrapping Up

• Write many tests

• Each test verifies a small chunk of code

• Don’t test everything

Page 95: Unit Testing JavaScript Applications

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

Page 96: Unit Testing JavaScript Applications

Thanks For Listening

• Ynon Perek

• http://ynonperek.com

[email protected]