Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Unit Testing JavaScript and Backbone.JS

David Mosher
November 05, 2011

Unit Testing JavaScript and Backbone.JS

Why do we hate testing? How do we test JavaScript? How do we test Backbone.JS? (Credits to @searls for slides 6 through 12, used with permission).

David Mosher

November 05, 2011
Tweet

More Decks by David Mosher

Other Decks in Programming

Transcript

  1. because YOUR CODE CAN SUCK TESTS IF YOU HAVE sort

    of ... Saturday, 5 November, 11
  2. YOUR CODE CAN SUCK TESTS IF YOU HAVE sort of

    ... Saturday, 5 November, 11
  3. with tests my code can suck safety net freedom to

    unsuck Saturday, 5 November, 11
  4. with tests my code can suck safety net freedom to

    unsuck change verified Saturday, 5 November, 11
  5. with tests my code can suck safety net freedom to

    unsuck change verified commit Saturday, 5 November, 11
  6. if it talks to the db not a unit test

    Saturday, 5 November, 11
  7. if it talks to the db talks across network not

    a unit test Saturday, 5 November, 11
  8. if it talks to the db talks across network won’t

    run out of order not a unit test Saturday, 5 November, 11
  9. if it talks to the db talks across network touches

    the filesystem won’t run out of order not a unit test Saturday, 5 November, 11
  10. security persisting data analytics application logic HTML templating routing users

    proxying UI events things servers are great at Saturday, 5 November, 11
  11. security persisting data analytics application logic HTML templating routing users

    proxying things servers are great at Saturday, 5 November, 11
  12. application logic HTML templating routing users UI events it’s simpler

    when the client handles Saturday, 5 November, 11
  13. don’t hit your server test native DOM apis write async

    specs test jQuery Saturday, 5 November, 11
  14. <a href="#" class="logout">Logout</a> $(‘.logout’).click(function() { alert('cats'); }); it(‘binds the click

    event’, function() { spyOn(window, ‘alert’); $(‘.logout’).trigger(‘click’); // yuck expect(window.alert).toHaveBeenCalledWith(‘cats’); }); don’t test jQuery Saturday, 5 November, 11
  15. <a href="#" class="logout">Logout</a> var logout = function() { alert('cats'); };

    $(‘.logout’).click(logout); don’t test jQuery Saturday, 5 November, 11
  16. <a href="#" class="logout">Logout</a> var logout = function() { alert('cats'); };

    $(‘.logout’).click(logout); it(‘says cats’, function() { spyOn(window, ‘alert’); logout(); // test the unit directly expect(window.alert).toHaveBeenCalledWith(‘cats’); }); don’t test jQuery Saturday, 5 November, 11
  17. don’t hit your server var getDogs = function() { $.post(‘/get/dog’,

    function(dog) { $(‘ul.dog’).append(‘<li class=”dog”>’ + dog + ‘</li>’); }); }; don’t write async specs Saturday, 5 November, 11
  18. don’t hit your server var getDogs = function() { $.post(‘/get/dog’,

    function(dog) { $(‘ul.dog’).append(‘<li class=”dog”>’ + dog + ‘</li>’); }); }; it(‘hits my server’, function() { runs(function() { getDogs(); // yuck }); waits(500); // yuck runs(function() { // yuck expect($(‘ul.dogs’)).toContain(‘li.dog’); }); }); don’t write async specs Saturday, 5 November, 11
  19. var getDogs = function() { $.post(‘/get/dog’, renderDog); }; var renderDog

    = function(dog) { $(‘ul.dog’).append(‘<li class=”dog”>’ + dog + ‘</li>’); } don’t hit your server don’t write async specs Saturday, 5 November, 11
  20. var getDogs = function() { $.post(‘/get/dog’, renderDog); }; var renderDog

    = function(dog) { $(‘ul.dog’).append(‘<li class=”dog”>’ + dog + ‘</li>’); } it(‘fetches a dog’, function() { // separation of concerns spyOn($, ‘post’); getDogs(); expect($.post).toHaveBeenCalledWith(‘/get/dog’, renderDogs); }); don’t hit your server don’t write async specs Saturday, 5 November, 11
  21. var getDogs = function() { $.post(‘/get/dog’, renderDog); }; var renderDog

    = function(dog) { $(‘ul.dog’).append(‘<li class=”dog”>’ + dog + ‘</li>’); } it(‘fetches a dog’, function() { // separation of concerns spyOn($, ‘post’); getDogs(); expect($.post).toHaveBeenCalledWith(‘/get/dog’, renderDogs); }); it(‘renders a dog’, function() { // isolated units renderDog(‘charlie’); expect($(ul.dog).find(‘.dog’)).toHaveText(‘charlie’); }); don’t hit your server don’t write async specs Saturday, 5 November, 11
  22. don’t test native DOM apis var loginView = function() {

    window.location.pathname = ‘/login’; }; Saturday, 5 November, 11
  23. don’t test native DOM apis var loginView = function() {

    window.location.pathname = ‘/login’; }; it(‘messes up my test runner’, function() { loginView(); expect(window.location.pathname).toBe(‘/login’); }); Saturday, 5 November, 11
  24. don’t test native DOM apis var redirectTo = function(spot) {

    // create a wrapper window.location.pathname = spot; } var loginView = function() { redirectTo(‘/login’); // use the wrapper in our unit }; Saturday, 5 November, 11
  25. don’t test native DOM apis var redirectTo = function(spot) {

    // create a wrapper window.location.pathname = spot; } var loginView = function() { redirectTo(‘/login’); // use the wrapper in our unit }; it(‘redirects to /login’, function() { spyOn(window, ‘redirectTo’); // mock our dependency loginView(); expect(redirectTo).toHaveBeenCalledWith(‘/login’); // win :) }); Saturday, 5 November, 11
  26. do spyOn($, ‘ajax’); mock html templates write and test wrappers

    test the “plumbing” Saturday, 5 November, 11
  27. dospyOn($, ‘ajax’); var getDogs = function() { $.ajax({ // declarative

    and explicit arguments url: ‘/get/dog’, success: renderDog }); }; Saturday, 5 November, 11
  28. dospyOn($, ‘ajax’); var getDogs = function() { $.ajax({ // declarative

    and explicit arguments url: ‘/get/dog’, success: renderDog }); }; it(‘fetches a dog’, function() { // separation of concerns spyOn($, ‘ajax’); getDogs(); expect($.ajax).toHaveBeenCalledWith({ url: ‘/get/dog’, success: renderDogs }); }); Saturday, 5 November, 11
  29. <script type=”text/html” id=”user-tpl”> <li class=”user”>{{name}}</li> </script> <ul class=”users”></ul> var renderUser

    = function(name) { var t = _.template($(‘#user-tpl’).html()); // probably elsewhere $(‘.users’).append(t({name:name}); } do mock html templates dotest the “plumbing” Saturday, 5 November, 11
  30. <script type=”text/html” id=”user-tpl”> <li class=”user”>{{name}}</li> </script> <ul class=”users”></ul> var renderUser

    = function(name) { var t = _.template($(‘#user-tpl’).html()); // probably elsewhere $(‘.users’).append(t({name:name}); } it(‘verifies too much and depends on the real DOM’, function() { var $users = $(‘.users’); renderUser(‘nate’); expect($users).toContain(‘li.user’); // limits refactoring expect($users.find(‘.user’)).toHaveText(‘nate’); }); do mock html templates dotest the “plumbing” Saturday, 5 November, 11
  31. <script type=”text/html” id=”user-tpl”> <li class=”user”>{{name}}</li> </script> <ul class=”users”></ul> var renderUser

    = function(name) { var t = _.template($(‘#user-tpl’).html()); $(‘.users’).append(t({name:name}); } do mock html templates dotest the “plumbing” Saturday, 5 November, 11
  32. <script type=”text/html” id=”user-tpl”> <li class=”user”>{{name}}</li> </script> <ul class=”users”></ul> var renderUser

    = function(name) { var t = _.template($(‘#user-tpl’).html()); $(‘.users’).append(t({name:name}); } it(‘tests the plumbing’, function() { // tests only the keys/vals var $users = inject(‘ul.users’); // inject isolates btwn tests injectTemplate(‘#user-tpl’, [‘name’]); // emits {{name}} renderUser(‘nate’); expect($users).toHaveText(‘nate’); // refactor markup easier }); do mock html templates dotest the “plumbing” Saturday, 5 November, 11
  33. do write and test wrappers var redirectTo = function(spot) {

    // create a wrapper window.location.pathname = spot; } var loginView = function() { redirectTo(‘/login’); // use the wrapper in our unit }; Saturday, 5 November, 11
  34. do write and test wrappers var redirectTo = function(spot) {

    // create a wrapper window.location.pathname = spot; } var loginView = function() { redirectTo(‘/login’); // use the wrapper in our unit }; it(‘redirects to /login’, function() { spyOn(window, ‘redirectTo’); // mock our dependency loginView(); expect(window.redirectTo).toHaveBeenCalledWith(‘/login); // win :) }); Saturday, 5 November, 11
  35. limit callback nesting $.post(‘/level/1’, function(shallow) { $.post(‘/level/2’, function(deep) { doSomething(shallow);

    $.post(‘/level/3’, function(deeper) { doSomething(deep); $.post(‘/level/4’, function(deepest) { doSomething(deeper); doSomething(deepest); console.log(“INCEPTION”); }); }); }); }); // what is this i don’t even Saturday, 5 November, 11
  36. limit callback nesting $.post(‘/level/1’, function(shallow) { APP.trigger(‘level-one-complete’, shallow); }); APP.bind(‘level-one-complete’,

    function(shallow) { doSomething(shallow); $.post(‘/level/2’, function(deep) { APP.trigger(‘level-two-complete’, deep); }); }); Saturday, 5 November, 11
  37. limit callback nesting $.post(‘/level/1’, function(shallow) { APP.trigger(‘level-one-complete’, shallow); }); APP.bind(‘level-one-complete’,

    function(shallow) { doSomething(shallow); $.post(‘/level/2’, function(deep) { APP.trigger(‘level-two-complete’, deep); }); }); etc... (but don’t use $.post!) Saturday, 5 November, 11
  38. limit anonymous functions $.ajax({ url: ‘/identity/crisis’, success: function(r) { //

    one (function(whoami) { // two alert(‘whoknows: ‘ + whoami); })(r); }, error: function(r) { // three (function(lostinspace) { // four alert(‘unknown: ‘ + lostinspace); })(r); } }); // anonymous overload! Saturday, 5 November, 11
  39. limit anonymous functions $.ajax({ url: ‘/identity/clarified’, success: whoAmI, error: lostInSpace

    }); // readability++ var whoAmI = function(identity) { alert(‘identified!: ‘ + identity); }; // testability++ var lostInSpace = function(ohnoes) { alert(‘lost!: ‘ + ohnoes); }; Saturday, 5 November, 11
  40. limit DOM verification <div id=”happy”> <div id=”and”> <div id=”knows”></div> <div

    id=”it”></div> </div> </div> describe(‘unnecessary verification’, function() { var $happy = $(“#happy”); expect($happy).toContain(“#and”); expect($happy).toContain(“#knows”); expect($happy).toContain(“#it”); }); // entirely unnecessary Saturday, 5 November, 11
  41. limit DOM verification <ul class=”celebs”> {{{a-list celebs}}} // triple curly

    = escape html </ul> Handlebars.registerHelper(‘a-list’, function(celebs) { var output; _.each(celebs, function(celeb) { output += ‘<li>’ + celeb.name + ‘</li>’; }); return output; }); // testable unit, acceptable Saturday, 5 November, 11
  42. MODELS APP.Models.User = Backbone.Model.extend({ defaults: { first: ‘Ralph’, last: ‘Wiggum’

    } }); var me = new APP.Models.User({ first: ‘Dave’, last: ‘Mosher’ }); Saturday, 5 November, 11
  43. VIEWS APP.Views.User = Backbone.View.extend({ el: ‘.user’, initialize: function() { this.model.bind(‘change’,

    this.updateUI); }, updateUI: function(model) { this.$(‘.first’).text(model.get(‘first’)); this.$(‘.last’).text(model.get(‘last’)); } }); Saturday, 5 November, 11
  44. VIEWS APP.Views.User = Backbone.View.extend({ el: ‘.user’, initialize: function() { this.model.bind(‘change’,

    this.updateUI); }, updateUI: function(model) { this.$(‘.first’).text(model.get(‘first’)); this.$(‘.last’).text(model.get(‘last’)); } }); new APP.Views.User({ model: new APP.Models.User }); me.set({first:‘Homer’, last:‘Simpson’); Saturday, 5 November, 11
  45. VIEWS APP.Views.User = Backbone.View.extend({ el: ‘.user’, initialize: function() { this.model.bind(‘change’,

    this.updateUI); }, updateUI: function(model) { this.$(‘.first’).text(model.get(‘first’)); this.$(‘.last’).text(model.get(‘last’)); } }); new APP.Views.User({ model: new APP.Models.User }); me.set({first:‘Homer’, last:‘Simpson’); <div class=”user”> <span class=”first”>Ralph</span> <span class=”last”>Wiggum</span> </div> Saturday, 5 November, 11
  46. VIEWS APP.Views.User = Backbone.View.extend({ el: ‘.user’, initialize: function() { this.model.bind(‘change’,

    this.updateUI); }, updateUI: function(model) { this.$(‘.first’).text(model.get(‘first’)); this.$(‘.last’).text(model.get(‘last’)); } }); new APP.Views.User({ model: new APP.Models.User }); me.set({first:‘Homer’, last:‘Simpson’); Saturday, 5 November, 11
  47. VIEWS APP.Views.User = Backbone.View.extend({ el: ‘.user’, initialize: function() { this.model.bind(‘change’,

    this.updateUI); }, updateUI: function(model) { this.$(‘.first’).text(model.get(‘first’)); this.$(‘.last’).text(model.get(‘last’)); } }); new APP.Views.User({ model: new APP.Models.User }); me.set({first:‘Homer’, last:‘Simpson’); <div class=”user”> <span class=”first”>Homer</span> <span class=”last”>Simpson</span> </div> Saturday, 5 November, 11
  48. VIEW EVENT HANDLERS APP.myView = new (Backbone.View.extend({ el: ‘#header .auth-widget’,

    events: { ‘click .login’ : ‘login’, ‘click .logout’ : ‘logout’ } }))(); Saturday, 5 November, 11
  49. testing events describe(‘myView.events’, function() { it(‘defines these events’, function() {

    var expected = { ‘click .login’ : ‘login’, ‘click .logout’ : ‘logout’ }; expect(APP.myView.events).toEqual(expected); }); }); Saturday, 5 November, 11
  50. view functions <a class=‘login’ data-name=‘dave’>login</a> login: function(e) { var $node

    = $(e.currentTarget); var name = $node.data(‘name’); alert(‘hi, ’ + name); } Saturday, 5 November, 11
  51. testing view functions describe(‘myView.login’, function() { it(‘says my name’, function()

    { spyOn(window, ‘alert’); var e = { currentTarget: $(‘<a data-name=”dave”></a>’); }; myView.login(e); expect(window.alert).toHaveBeenCalledWith(‘hi, dave’); }); }); Saturday, 5 November, 11
  52. testing view functions describe(‘myView.login’, function() { it(‘says my name’, function()

    { spyOn(window, ‘alert’); var e = { currentTarget: $(‘<a data-name=”dave”></a>’); }; myView.login(e); expect(window.alert).toHaveBeenCalledWith(‘hi, dave’); }); }); // arrange Saturday, 5 November, 11
  53. testing view functions describe(‘myView.login’, function() { it(‘says my name’, function()

    { spyOn(window, ‘alert’); var e = { currentTarget: $(‘<a data-name=”dave”></a>’); }; myView.login(e); expect(window.alert).toHaveBeenCalledWith(‘hi, dave’); }); }); // arrange // act Saturday, 5 November, 11
  54. testing view functions describe(‘myView.login’, function() { it(‘says my name’, function()

    { spyOn(window, ‘alert’); var e = { currentTarget: $(‘<a data-name=”dave”></a>’); }; myView.login(e); expect(window.alert).toHaveBeenCalledWith(‘hi, dave’); }); }); // arrange // act // assert Saturday, 5 November, 11
  55. routers APP.Router = new (Backbone.Router.extend({ routes: { ‘/logout/:user’ : ‘logout’

    }, logout: function(user) { alert(‘bye, ‘ + user); } }))(); Saturday, 5 November, 11
  56. testing routes describe(‘APP.Router.logout’, function() { it(‘says bye’, function() { //

    arrange spyOn(window, ‘alert’); var router = APP.Router; // act router.logout(‘dave’); // unit router.navigate(‘/logout/dave’, true); // integration // assert expect(window.alert).toHaveBeenCalledWith(‘bye, dave’); }); }); Saturday, 5 November, 11
  57. models APP.User = new (Backbone.Model.extend({ defaults: { name: ‘default’ },

    initialize: function() { this.bind(‘change:name’, this.render); }, render: function(model, name) { alert(name); } }))(); Saturday, 5 November, 11
  58. testing models describe(‘APP.User.render’, function() { it(‘says my name’, function() {

    // arrange spyOn(window, ‘alert’); var user = APP.User; // act user.render(jasmine.any(Object), ‘ralph’); // unit user.set({ name: ‘ralph’ }); // integration // assert expect(window.alert).toHaveBeenCalledWith(‘ralph’); }); }); Saturday, 5 November, 11
  59. jasmine-stealth beforeEach(function() { someSpy = jasmine.createSpy(); someSpy.when("panda",1).thenReturn("sad"); }); it("stubs accurately",

    function() { expect(someSpy("panda", 1)).toBe("sad")); }); also by @searls Saturday, 5 November, 11
  60. jasmine-jquery var $node = inject(‘#some-id’) expect($node).toBe('div#some-id') by @velesin var $a

    = $(‘<a id=”foo”>barcamp!</a>’) expect($a).toHaveText('barcamp!') Saturday, 5 November, 11
  61. tryjasmine.com by ... @searls compiles js and coffeescript saves specs

    between sessions (localStorage) Saturday, 5 November, 11