Click here to load reader
Upload
ynon-perek
View
1.462
Download
103
Embed Size (px)
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