Post on 07-May-2015
description
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