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

Unit Testing JavaScript when you're Afraid of JavaScript

Unit Testing JavaScript when you're Afraid of JavaScript

Presented at Nebraska Code Camp in March 2013

Video available at https://vimeo.com/62004647

Matt Steele

March 16, 2013
Tweet

More Decks by Matt Steele

Other Decks in Programming

Transcript

  1. > Math.max() -Infinity > Math.min() Infinity > Math.max() > Math.min()

    Sometimes it’s the language. Anyone want to guess what this does?
  2. > Math.max() -Infinity > Math.min() Infinity > Math.max() > Math.min()

    false Sometimes it’s the language. Anyone want to guess what this does?
  3. > "I am a string" instanceof String false > String("abc")

    instanceof String How about object construction?
  4. > "I am a string" instanceof String false > String("abc")

    instanceof String false How about object construction?
  5. > "I am a string" instanceof String false > String("abc")

    instanceof String false > (new String("abc")) instanceof String How about object construction?
  6. > "I am a string" instanceof String false > String("abc")

    instanceof String false > (new String("abc")) instanceof String true How about object construction?
  7. > "I am a string" instanceof String false > String("abc")

    instanceof String false > (new String("abc")) instanceof String true > String("abc") == (new String("abc")) How about object construction?
  8. > "I am a string" instanceof String false > String("abc")

    instanceof String false > (new String("abc")) instanceof String true > String("abc") == (new String("abc")) true How about object construction?
  9. Every number is a floating point, which means 1.0 +

    1.0 does not equal what you think it does.
  10. How about crazy APIs? You’d think that new-ing an Array

    with 4 would initialize with that value. Nope. Creates 4 empty values. Oh, and type coercion is pretty awesome too.
  11. > ",,," == new Array(4) How about crazy APIs? You’d

    think that new-ing an Array with 4 would initialize with that value. Nope. Creates 4 empty values. Oh, and type coercion is pretty awesome too.
  12. > ",,," == new Array(4) true How about crazy APIs?

    You’d think that new-ing an Array with 4 would initialize with that value. Nope. Creates 4 empty values. Oh, and type coercion is pretty awesome too.
  13. The browser is the most hostile software engineering environment imaginable.

    - Douglas Crockford So let’s take this nitroglycerin and throw it into the Fire Swamp. We’ll never survive. Nonsense, you’re only saying that because no one ever has.
  14. CAN I VERTICALLY CENTER MY CONTENT PLEASE? W3C changed the

    spec and there are 3 revisions. No polyfill for the latest. This stuff’s crazy
  15. Even when something gets implemented everywhere, you run into problems.

    LocalStorage sounds great Alternative? IndexedDB - no mobile, IE < 10
  16. Sometimes you just want to give up CSS - ever

    happened to you? Gives you 47 minutes. CSS Animations -> jQuery, etc. It’s amazing we can build anything at all.
  17. Our definitive guidebook is a primer on which parts of

    the language *not* to use. Does this seem weird?
  18. Battle Plan Give up? Hell no. But to get out

    alive, we need to have a strategy My secret weapon:
  19. Testing Fail as quickly as possible. No static types or

    compiler Test all the time. Test everything. Treat tests as your compiler. Test as your linter. Live and die by it.
  20. Tools Code Tips The tools I use for testing how

    it changes the code you write some tips on how to get started
  21. Tools Code Tips The tools you use, how it changes

    the code you write, and some tips on how to get started
  22. You might have done testing with Selenium or other point-n-click

    tools. I call these “integration” or “system” tests.
  23. Problems Google ended up writing thousands for GMail. Turns out,

    it’s unsustainable. Took hours for test suites to run Can’t rely on them as a safety net
  24. Problems Google ended up writing thousands for GMail. Turns out,

    it’s unsustainable. Took hours for test suites to run Can’t rely on them as a safety net
  25. •Slow Problems Google ended up writing thousands for GMail. Turns

    out, it’s unsustainable. Took hours for test suites to run Can’t rely on them as a safety net
  26. •Slow •Depends on a browser Problems Google ended up writing

    thousands for GMail. Turns out, it’s unsustainable. Took hours for test suites to run Can’t rely on them as a safety net
  27. •Slow •Depends on a browser •Brittle Problems Google ended up

    writing thousands for GMail. Turns out, it’s unsustainable. Took hours for test suites to run Can’t rely on them as a safety net
  28. •Slow •Depends on a browser •Brittle •Coupled to implementation Problems

    Google ended up writing thousands for GMail. Turns out, it’s unsustainable. Took hours for test suites to run Can’t rely on them as a safety net
  29. •Slow •Depends on a browser •Brittle •Coupled to implementation •Debugging

    woes Problems Google ended up writing thousands for GMail. Turns out, it’s unsustainable. Took hours for test suites to run Can’t rely on them as a safety net
  30. Unit Testing Spend the rest of the time discussing Unit

    Testing. Don’t test your system. Instead, test individual units of functionality Write lots of unit tests, each testing one thing
  31. describe("A test suite", function() { var a; it("can perform assertions",

    function() { a = true; expect(a).toBe(true); }); }); Lots out there. I like Jasmine. Similar to RSpec. Reads like a sentence: - I expect a to be true
  32. it("does regular expressions", function() { var message = 'foo bar

    baz'; expect(message).toMatch(/bar/); expect(message).toMatch('bar'); expect(message).not.toMatch(/quux/); }); Has tons of assertions built in, including regular expressions
  33. var makeExcited = function(str) { if (str) { return str.toUpperCase()

    + '!!1'; } } Here’s how you’d use it. I’ve written a function to make a string excited.
  34. describe('Make Excited', function() { it('uppercases and adds exclamations', function() {

    var excited = makeExcited('nebraska code camp'); expect(excited).toBe('NEBRASKA CODE CAMP!!1'); }; it('handles no value gracefully', function() { expect(makeExcited()).toBeUndefined(); }; }); Here’s what its test would look like. You can test “happy path” But the key is testing edge cases. What happens if you pass an object?
  35. As you build your tests into suites, you can run

    them. Really fast - took 18 milliseconds to run all your tests At this speed, you can have your tests running every time you save
  36. Open-source, so people have built on top of this. jasmine-jquery:

    worth checking out Lots of assertions to make your tests more descriptive
  37. Some of these are nuts. ImageDiff lets you test out

    Canvas code by passing in two images It compares the images, and whatever’s different shows up like this monocle.
  38. Another nice tool is Sinon. Fake out Ajax calls Timers,

    so you can test your setTimeout methods directly More flushed mock objects
  39. Tools Code Tips How do you integrate this into your

    workflow? Not as simple as you’d think
  40. jQuery(function($) { }); How to make the form submit code

    work. Using jQuery To start, we’ll be using jQuery and binding to the Document.Ready
  41. jQuery(function($) { $('form').on('submit', function() { }); }); We want to

    bind to when the form submits, something happens
  42. jQuery(function($) { $('form').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', }); return false; }); }); We return false, and make an ajax request. We specify the endpoint, method, and what gets returned.
  43. jQuery(function($) { $('form').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', data: {text: $(this).find('textarea').val()}, }); return false; }); }); We also want to pass data, so we’ll do a query to find the text inside the textarea
  44. jQuery(function($) { $('form').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses') .append('<li>' + data.text + '</li>'); } }); return false; }); }); And when the ajax call returns, we expect it’ll contain a “text” JSON field. We then find our #statuses field and create a new list item containing what got returned
  45. (ಥ_ಥ) A couple test cases (empty data, fast submission, errors),

    each requiring duplicate setup Also, need to pull in the HTML or duplicate it verbatim? Ugh
  46. (! ಠӹಠ) ! WHY IS THIS SO HARD?! SERIOUSLY I

    JUST WANT TO TEST A FORM SUBMISSION
  47. Doesn’t Happen Sad truth is, this is way too common.

    And means we don’t test our code.
  48. Lincoln GitHub - find users with location Pull down repos

    Check for ‘test’ or ‘spec’ files Probably overestimating.
  49. Unit testing JavaScript is hard when we write bad code

    Problem is, this is bad code. Any other programming language, we’d stop and refactor. Who was coding in something other than JS 5 years ago? Lots of us came to JS from other languages. We test there, right?
  50. Single Responsibility Principle Concept in OOP called the SRP Every

    module should have a single responsibility, and that responsibility should be entirely encapsulated by that module.
  51. jQuery(function($) { $('form').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); Violates is so many ways Bound to page loading, so you only have one chance to test it Tied to form submission, so you have to do that Requires network activities, a textarea to have been filled in, an event triggered by successful ajax, and finally HTML templating.
  52. jQuery(function($) { $('form').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); Violates is so many ways Bound to page loading, so you only have one chance to test it Tied to form submission, so you have to do that Requires network activities, a textarea to have been filled in, an event triggered by successful ajax, and finally HTML templating.
  53. jQuery(function($) { $('form').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); Page Load Violates is so many ways Bound to page loading, so you only have one chance to test it Tied to form submission, so you have to do that Requires network activities, a textarea to have been filled in, an event triggered by successful ajax, and finally HTML templating.
  54. jQuery(function($) { $('form').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); Page Load Form Event Violates is so many ways Bound to page loading, so you only have one chance to test it Tied to form submission, so you have to do that Requires network activities, a textarea to have been filled in, an event triggered by successful ajax, and finally HTML templating.
  55. jQuery(function($) { $('form').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); Page Load Form Event Network I/O Violates is so many ways Bound to page loading, so you only have one chance to test it Tied to form submission, so you have to do that Requires network activities, a textarea to have been filled in, an event triggered by successful ajax, and finally HTML templating.
  56. jQuery(function($) { $('form').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); Page Load Form Event Network I/O User Input Violates is so many ways Bound to page loading, so you only have one chance to test it Tied to form submission, so you have to do that Requires network activities, a textarea to have been filled in, an event triggered by successful ajax, and finally HTML templating.
  57. jQuery(function($) { $('form').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); Page Load Form Event Network I/O Network Event User Input Violates is so many ways Bound to page loading, so you only have one chance to test it Tied to form submission, so you have to do that Requires network activities, a textarea to have been filled in, an event triggered by successful ajax, and finally HTML templating.
  58. jQuery(function($) { $('form').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); Page Load Form Event Network I/O Network Event User Input Templating Violates is so many ways Bound to page loading, so you only have one chance to test it Tied to form submission, so you have to do that Requires network activities, a textarea to have been filled in, an event triggered by successful ajax, and finally HTML templating.
  59. 6 Things to Test Only 14 lines of code. Dense,

    in the same way a ball of yarn is dense.
  60. 6 Things to Change If any of them changes, all

    your tests fail. If all tests fail, you can’t know what’s caused a breakage
  61. 6 Things to Fail At this point, might as well

    write integration/selenium tests. Not getting any benefit from them.
  62. jQuery(function($) { $('form').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); Let’s return to the code. How could we fix it?
  63. var addStatus = function() { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', data: {text: $('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); }; jQuery(function($) { $('form').on('submit', function() { addStatus(); return false; }); }); First step, separate ajax behvaior from document.ready. Moved into addStatus function
  64. var addStatus = function(options) { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', data: { text: options.text }, success: options.success }); }; jQuery(function($) { $('form').on('submit', function() { addStatus({ text: $('textarea').val(), success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); Still having to do lots of DOM queries inside addStatus. Let’s have it be handed the text it should be sending, as well as the jQuery callback Now addStatus is solely focused on handing ajax, nothing else.
  65. var Statuses = function() {}; Statuses.prototype.add = function(options) { $.ajax({

    url: '/statuses', type: 'POST', dataType: 'json', data: { text: options.text }, success: options.success }); }; Let’s keep going. We can create a Model object Using design pattern called Constructor with Prototype Functions are objects; can now manage it. Easier to reuse model if we want to. Encapsulate data
  66. jQuery(function($) { var statuses = new Statuses(); $('form').on('submit', function() {

    statuses.add({ text: $('textarea').val(), success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); Here’s what document.ready looks like. Newing up a Statuses model
  67. var NewStatusView = function(options) { var statuses = options.statuses; $('form').on('submit',

    function() { statuses.add({ text: $('textarea').val(), success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }; jQuery(function($) { var statuses = new Statuses(); new NewStatusView({statuses: statuses}); }); Still have lots inside page load. Let’s fix that Create a View object called NewStatusView Pass it our model object “statuses”. Now page load just assembles the elements together
  68. var NewStatusView = function(options) { this.statuses = options.statuses; $('form').on('submit', $.proxy(this.addStatus,

    this)); }; NewStatusView.prototype.addStatus = function(e) { e.preventDefault(); var that = this; this.statuses.add({ text: $('textarea').val(), success: function(data) { that.appendStatus(data.text); } }); return false; }; NewStatusView.prototype.appendStatus = function(text) { $('#statuses').append('<li>' + text + '</li>'); }; We can structure the view more. Extract the HTML templating to appendStatus Move the addStatus code to its own function JS changes “this” inside member functions. Rules are confusing, we use $.proxy here
  69. describe('Monologue', function() { beforeEach(function() { this.server = sinon.fakeServer.create(); }); it('has

    a view with a model', function() { var statuses = new Statuses(), submitted = false; this.server.respondWith('{"text" : "Testing"}'); statuses.add({ text: 'Testing', success: function() { submitted = true; } }); this.server.respond(); expect(submitted).toBe(true); }); Here’s what a test looks like. Test our model. Fake out a server using Sinon Important code is where we new up Statuses and call Add. Totally isolated from the view. Can test the ajax behavior directly.
  70. describe('Status View', function() { it('is tied to Statuses', function() {

    var statuses = { add: function() {} }; var fixture = $('<div><form><textarea /></form></div>'); spyOn(statuses, 'add'); var newStatusView = new NewStatusView({ statuses: statuses, el: fixture }); fixture.find('form').trigger('submit'); expect(statuses.add).toHaveBeenCalled(); }); }); Testing our view is similar. We’re creating a fake version of our model Using Jasmine feature called spies to do this Can verify the view called Add on the spy Also: we’re not tied to document.ready. We test the view in isolation
  71. Let’s keep going Not a bad starting point. Should keep

    going. We expanded from 16 lines to 40. I think it’s better Each line is single responsibility. Could break it down further. Frameworks can help with this.
  72. var Monologue = { Model: {}, View: {}, Collection: {}

    }; Monologue.Model.Status = Backbone.Model.extend({}); Monologue.Collection.Statuses = Backbone.Collection.extend({ model: Monologue.Model.Status, url: '/statuses' }); Not a backbone class Lots of concepts apply. Example: Backbone has a Model with REST-style sync. Just specify collection and endpoint URL
  73. Monologue.View.PostStatus = Backbone.View.extend({ events: { 'submit' : 'submit' }, submit:

    function(e) { var $input = this.$el.find('textarea'); this.collection.create({text: $input.val()}); return false; } }); Views can say “on form submit, trigger submit code in view” You pass in the form, it’s responsible for creating a new collection based on data All totally encapsulated
  74. Monologue.View.StatusList = Backbone.View.extend({ initialize: function() { this.collection.on('add', this.add); this.collection.fetch(); },

    add: function(model) { this.$el.append(this.template(model)); }, template: function(model) { return this.make('li', {'class':'status'}, model.get('text')); } }); Adding to collection triggers events Can add another view that’s bound to this collection. When items added, work gets done Templating is really straightforward too
  75. jQuery(function($) { var statuses = new Monologue.Collection.Statuses(); new Monologue.View.PostStatus({ el:

    $('#new-status'), collection: statuses }); new Monologue.View.StatusList({ el: $('#statuses'), collection: statuses }); }); Here’s how you set up your app. Again, just creating models and injecting views into our code
  76. describe('Post Status Form', function() { var view, $el, collection; beforeEach(function()

    { $el = $("<form><textarea>Whee!</textarea></form>"); collection = new (Backbone.Collection.extend({ url: '/mock' })); spyOn(collection, 'create'); view = new Monologue.View.PostStatus({ el: $el, collection: collection }); }); }); Here’s what testing the view looks like We create a view with a fake Backbone collection and textarea
  77. describe("submitting the form", function() { it("creates status when submitting the

    form", function() { $el.trigger('submit'); expect(collection.create) .toHaveBeenCalledWith({text: "Whee!"}); }); }); And by submitting the form, we verify the collection got a new element All the same concepts, now you’re just reducing the amount of code you have to write Testing models - way easier Can rely on dozens of tests from API. Don’t have to test ajax; they’ve done it!
  78. Putting your tests to work Will have a large test

    suite. Here’s how you make it work.
  79. Grunt Has a test runner for Jasmine - in headless

    webkit Does way more; could spend an entire talk on it “Watch” - every time tests change, run
  80. Testacular Need to test in real browsers Testacular runs on

    everything you have, including PhantomJS Has Grunt plugin too, so can spin them up from CLI
  81. Tools Code Tips The tools you use, how it changes

    the code you write, and some tips on how to get started
  82. 1. Start Small Easier on greenfield projects Traditionally designed code

    = hard Start small side project & test from the beginning Do simple exercises. Project euler, code katas, etc
  83. 3. TDD Write failing test Write production code Forces you

    to design so it’s testable I write all my code this way
  84. 4. Synchronous first Jasmine can test async code But it’s

    tricky. Get your feet wet with sync code first Node is a good way to do this. Try out Node first!
  85. 5. Pair Work with someone. Especially if you’re both new

    Keeps you honest, you learn how to make it work
  86. You Don’t Have to be Afraid Anymore Don’t have to

    be scared if you do these things. Can worry about other things, like browser perf, IE7, working on mobile, semicolons... well at least don’t have to worry about testing.