Unit Testing JavaScript Applications

1,930 views
1,779 views

Published on

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

Published in: Technology, Business
0 Comments
5 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
1,930
On SlideShare
0
From Embeds
0
Number of Embeds
123
Actions
Shares
0
Downloads
29
Comments
0
Likes
5
Embeds 0
No embeds

No notes for slide

Unit Testing JavaScript Applications

  1. 1. Mocha First Steps Installing and running tests
  2. 2. Agenda • JS Unit Testing • A first Mocha test • Running tests with Karma • IDE integration
  3. 3. Getting Ready To Test • JS Unit tests (try) make sure our JS code works well
  4. 4. Project Tree index.html   - src   - main.js   - buttons.js   - player.js   - style   - master.css   - home.css  
  5. 5. Project Tree index.html test.html   - src   - main.js   - buttons.js   - player.js   - style   - master.css   - home.css   - spec   - test_button.js   - test_player.js
  6. 6. What Mocha Isn’t • No UI / CSS testing • No server testing
  7. 7. Testing How
  8. 8. Testing Libraries • Let’s try to write test program for Array • Verify indexOf(...) actually works
  9. 9. Array#indexof var arr1 = [10, 20, 30, 40];   if ( arr1.indexOf(20) === 1 ) {   console.log('success!'); } else {   console.log('error'); }
  10. 10. What Went Wrong • Hard to debug • Hard to run automatically
  11. 11. We Need …
  12. 12. We Need …
  13. 13. Testing Libraries • A testing library tells you how to structure your testing code • We’ll use mocha
 http://visionmedia.github.io/mocha/
  14. 14. Hello Mocha var 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);     } );   }); });
  15. 15. Hello Mocha • describe() defines a block • it() defines functionality
  16. 16. Assertions • Uses a separate assertions library • • I went with Chai http://chaijs.com/
  17. 17. Running Our Test: Karma
  18. 18. Meet Karma • A test runner for JS • Integrates with many IDEs • Integrates with CI servers • http://karma-runner.github.io/0.10/ index.html
  19. 19. Karma Architecture Karma Server
  20. 20. 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
  21. 21. Karma Config • Just a JavaScript file • keys determine how test should run
  22. 22. Karma Config • files is a list of JS files to include in the test • Can use wildcards
  23. 23. Karma Config • browsers is a list of supported browsers
  24. 24. Running Tests # start a karma server karma start   # execute tests karma run
  25. 25. IDE Integration
  26. 26. What We Learned • Mocha is a JS library that helps us write unit tests • Karma is a JS library that helps us run them
  27. 27. Q &A
  28. 28. Advanced Mocha How to write awesome tests
  29. 29. Agenda • Flow control: before, after, beforeEach, afterEach • Writing async tests • Fixtures and DOM testing
  30. 30. Let’s Flow describe('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   }); });
  31. 31. Let’s Flow describe('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...
  32. 32. A Better Scheme • beforeEach() runs before each test • also has: • • afterEach() for cleanups 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!   });! ! before() and after() run once in the suite  !   it('should do Y', function() {!     // test stuff with game!   });! });
  33. 33. Async Testing
  34. 34. Async Theory var x = 10 test x x has the right value testing here is OK
  35. 35. Async Theory $.get(...) test result Can’t test now, result not yet ready
  36. 36. Async Theory • Async calls take callbacks • We should tell mocha to wait
  37. 37. Async Code describe('Test 1', function() {     it('should do wait', function(done) {     setTimeout(function() {       // now we can test result       assert(true);       done();     }, 1000);   });   });
  38. 38. Async Code Taking a function argument tells mocha the test will only end after it’s called describe('Test 1', function() {     it('should do wait', function(done) {     setTimeout(function() {       // now we can test result       assert(true);       done();     }, 1000);   });   });
  39. 39. Async Code Calling the callback ends the test describe('Test 1', function() {     it('should do wait', function(done) {     setTimeout(function() {       // now we can test result       assert(true);       done();     }, 1000);   });   });
  40. 40. Async Notes • Always call done() or your test will fail on timeout • Default timeout is 2 seconds
  41. 41. 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);       }); });
  42. 42. 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();     });   }); });
  43. 43. DOM Testing
  44. 44. Theory “Real” HTML body h1 div body img div “Test” HTML
  45. 45. Theory “Real” HTML body h1 div body img div “Test” HTML img
  46. 46. Theory images.js $('img.thumbnail').css({ width: 200, height: 200 }); fixture.html <img class="thumbmail" src="home.png" />
  47. 47. 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"]; }); !
  48. 48. Almost Ready • HTML files are not served by default • We need to tell karma to serve it
  49. 49. 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' ],
  50. 50. 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 ); });
  51. 51. Fixtures & DOM • Define DOM fragments in HTML files • Load from test suite • Test and clean up
  52. 52. Spying With Sinon Stubs, Spies and Mock Objects explained
  53. 53. Agenda • Reasons to mock • Vanilla mocking • How sinon can help • Stubs and Spies • Faking timers • Faking the server
  54. 54. Reasons To Mock
  55. 55. Reasons To Mock $.ajax setTimeout PersonalData
  56. 56. Reasons To Mock $.ajax setTimeout PersonalData
  57. 57. Reasons To Mock • PersonalData object can save data to server • If saving failed, it retries 3 times
  58. 58. Reasons To Mock • Both server and clock are external • We prefer to test in isolation
  59. 59. 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
  60. 60. 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
  61. 61. What We Can Do • Solution:
 https://gist.github.com/ynonp/6667284
  62. 62. Mocking Notes • Solution is far from perfect. • After the test our “fake” methods remain • Writing “fake” methods was not trivial
  63. 63. This Calls for Sinon
  64. 64. About Sinon • A JS mocking library • Helps create fake objects • Helps tracking them
  65. 65. About Sinon • Homepage:
 http://sinonjs.org/ • Google Group:
 http://groups.google.com/group/sinonjs • IRC Channel:
 #sinon.js on freenode
  66. 66. Solving with Sinon • Here’s how sinon might help us with the previous task • Code:
 https://gist.github.com/ynonp/6667378
  67. 67. Solution Notes • Sinon’s fake timer was easier to use than writing our own • Now we have a synchronous test (instead of async)
  68. 68. Let’s Talk About Sinon
  69. 69. Spies • A spy is a function that provides the test code with info about how it was used
  70. 70. Spies Demo describe('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); }); }); });
  71. 71. Spy On Existing Funcs describe('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 ); }); }); });
  72. 72. Spies Notes • Full API:
 http://sinonjs.org/docs/#spies • Tip: Use as callbacks
  73. 73. Spy + Action = Stub
  74. 74. Stub Demo • Let’s fix our starting example • We’ll replace $.ajax with a stub • That stub always fails
  75. 75. Stub Demo var 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); }); }); });
  76. 76. What Can Stubs Do var callback = sinon.stub(); ! callback.withArgs(42).returns(1); ! callback.withArgs(1).throws("TypeError"); !
  77. 77. Stubs API • Full Stubs API docs:
 http://sinonjs.org/docs/#stubs • Main actions: • return stuff • throw stuff • call stuff
  78. 78. Spies Lab • Given code here:
 https://gist.github.com/ynonp/7101081 • Fill in the blanks to make the tests pass
  79. 79. Fake Timers • Use sinon.useFakeTimers() to create a fake timer • Use clock.restore() to clear fake timers
  80. 80. Fake Timers • Use tick(...) to advance • Affected methods: • setTimeout, setInterval, clearTimeout, clearInterval • Date constructor
  81. 81. Fake Servers • Testing client/server communication is hard • Use fake servers to simplify it
  82. 82. Fake Servers $.ajax PersonalData
  83. 83. Fake Servers Fake $.ajax PersonalData
  84. 84. Let’s write a test for the following class 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. function Person(id) {   var self = this;     self.load = function() {     var url = '/users/' + id;       $.get('/users/' + id, function(info) {       self.name = info.name;       self.favorite_color = info.favorite_color;     });   }; }
  85. 85. Testing Plan • Set-up a fake server • Create a new Person • call load() • verify fields data
  86. 86. Setting Up The Server 1. 2. 3. 4. 5. 6. 7. 8. 9. var server = sinon.fakeServer.create();   var headers  = {"Content-Type" : "application/json"}; var response = JSON.stringify(                 {"name" : "joe", "favorite_color": "blue" });   server.respondWith("GET", "/users/7",                    [200, headers, response]); // now requesting /user/info.php returns joe's info as a JSON
  87. 87. Loading a Person 1. 2. 3. 4. 5. 6. var p = new Person(7); // sends a request p.load();   // now we have 1 pending request, let's fake the response server.respond();
  88. 88. Verifying the Data 1. 2. 3. 4. 5. 6. // finally, verify data expect(p.name).to.eq('joe'); expect(p.favorite_color).to.eq('blue');   // and restore AJAX behavior server.restore();
  89. 89. Fake Server • Use respondWith() to set up routes • Use respond() to send the response
  90. 90. Fake Server • Regexps are also supported, so this works: 1. 2. 3. 4. 5. 6. server.respondWith(//todo-items/(d+)/, function (xhr, id) {     xhr.respond(       200,       { "Content-Type": "application/json" },       '[{ "id": ' + id + ' }]'); });
  91. 91. Fake Server • For fine grained control, consider fake XMLHttpRequest • http://sinonjs.org/docs/#server
  92. 92. Wrapping Up
  93. 93. Wrapping Up • Unit tests work best in isolation • Sinon will help you isolate units, by faking their dependencies
  94. 94. Wrapping Up • Write many tests • Each test verifies a small chunk of code • Don’t test everything
  95. 95. 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
  96. 96. Thanks For Listening • Ynon Perek • http://ynonperek.com • ynon@ynonperek.com

×