Unit Testing JavaScript and Backbone.JS

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

Ead076bf445f9b50e3c094300e4690e9?s=128

David Mosher

November 05, 2011
Tweet

Transcript

  1. 37.

    because YOUR CODE CAN SUCK TESTS IF YOU HAVE sort

    of ... Saturday, 5 November, 11
  2. 38.

    YOUR CODE CAN SUCK TESTS IF YOU HAVE sort of

    ... Saturday, 5 November, 11
  3. 44.

    with tests my code can suck safety net freedom to

    unsuck Saturday, 5 November, 11
  4. 45.

    with tests my code can suck safety net freedom to

    unsuck change verified Saturday, 5 November, 11
  5. 46.

    with tests my code can suck safety net freedom to

    unsuck change verified commit Saturday, 5 November, 11
  6. 53.

    if it talks to the db not a unit test

    Saturday, 5 November, 11
  7. 54.

    if it talks to the db talks across network not

    a unit test Saturday, 5 November, 11
  8. 55.

    if it talks to the db talks across network won’t

    run out of order not a unit test Saturday, 5 November, 11
  9. 56.

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

    security persisting data analytics application logic HTML templating routing users

    proxying UI events things servers are great at Saturday, 5 November, 11
  11. 71.

    security persisting data analytics application logic HTML templating routing users

    proxying things servers are great at Saturday, 5 November, 11
  12. 78.
  13. 79.

    application logic HTML templating routing users UI events it’s simpler

    when the client handles Saturday, 5 November, 11
  14. 86.

    don’t hit your server test native DOM apis write async

    specs test jQuery Saturday, 5 November, 11
  15. 88.

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

    <a href="#" class="logout">Logout</a> var logout = function() { alert('cats'); };

    $(‘.logout’).click(logout); don’t test jQuery Saturday, 5 November, 11
  17. 90.

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

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

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

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

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

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

    don’t test native DOM apis var loginView = function() {

    window.location.pathname = ‘/login’; }; Saturday, 5 November, 11
  24. 98.

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

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

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

    do spyOn($, ‘ajax’); mock html templates write and test wrappers

    test the “plumbing” Saturday, 5 November, 11
  28. 106.

    dospyOn($, ‘ajax’); var getDogs = function() { $.ajax({ // declarative

    and explicit arguments url: ‘/get/dog’, success: renderDog }); }; Saturday, 5 November, 11
  29. 107.

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

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

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

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

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

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

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

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

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

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

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

    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
  41. 130.
  42. 131.

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

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

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

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

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

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

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

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

    VIEW EVENT HANDLERS APP.myView = new (Backbone.View.extend({ el: ‘#header .auth-widget’,

    events: { ‘click .login’ : ‘login’, ‘click .logout’ : ‘logout’ } }))(); Saturday, 5 November, 11
  51. 162.

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

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

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

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

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

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

    routers APP.Router = new (Backbone.Router.extend({ routes: { ‘/logout/:user’ : ‘logout’

    }, logout: function(user) { alert(‘bye, ‘ + user); } }))(); Saturday, 5 November, 11
  58. 173.

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

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

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

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

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

    tryjasmine.com by ... @searls compiles js and coffeescript saves specs

    between sessions (localStorage) Saturday, 5 November, 11