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. Unit Testing JavaScript When You’re Afraid Of JavaScript

  2. Confession Before we start

  3. I am SCARED of JavaScript

  4. Sometimes it’s the language. Anyone want to guess what this

    does?
  5. > Math.max() Sometimes it’s the language. Anyone want to guess

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

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

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

    Anyone want to guess what this does?
  9. > Math.max() -Infinity > Math.min() Infinity > Math.max() > Math.min()

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

    false Sometimes it’s the language. Anyone want to guess what this does?
  11. How about object construction?

  12. > "I am a string" instanceof String How about object

    construction?
  13. > "I am a string" instanceof String false How about

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

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

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

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

    instanceof String false > (new String("abc")) instanceof String true How about object construction?
  18. > "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?
  19. > "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?
  20. Every number is a floating point, which means 1.0 +

    1.0 does not equal what you think it does.
  21. 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.
  22. > ",,," == 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.
  23. > ",,," == 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.
  24. Bad advice everywhere.

  25. Bad advice everywhere.

  26. 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.
  27. 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
  28. Even when something gets implemented everywhere, you run into problems.

    LocalStorage sounds great Alternative? IndexedDB - no mobile, IE < 10
  29. 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.
  30. Everyone is self-taught More of a cultural issue

  31. Our definitive guidebook is a primer on which parts of

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

    alive, we need to have a strategy My secret weapon:
  33. 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.
  34. Matt Steele @mattdsteele http://matthew-steele.com

  35. Tools Code Tips The tools I use for testing how

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

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

    tools. I call these “integration” or “system” tests.
  38. 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
  39. 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
  40. •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
  41. •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
  42. •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
  43. •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
  44. •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
  45. 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
  46. 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
  47. 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
  48. 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.
  49. 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?
  50. 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
  51. When tests fail, you get descriptive info.

  52. Open-source, so people have built on top of this. jasmine-jquery:

    worth checking out Lots of assertions to make your tests more descriptive
  53. 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.
  54. Another nice tool is Sinon. Fake out Ajax calls Timers,

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

    workflow? Not as simple as you’d think
  56. Example Time Let’s take these tools and apply them to

    the simplest application ever.
  57. None
  58. 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
  59. jQuery(function($) { $('form').on('submit', function() { }); }); We want to

    bind to when the form submits, something happens
  60. 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.
  61. 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
  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; }); }); 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
  63. Now try to test that. Okay, so let’s write a

    unit test for that
  64. (ʘ‿ʘ) No problem, I’ll just start with... wait, document.ready. How

    do I repeat this every time?
  65. (ಠ_ಠ) Also need Ajax Same endpoint? Guess I can fake,

    but now have two things
  66. (ಥ_ಥ) 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
  67. (! ಠӹಠ) ! WHY IS THIS SO HARD?! SERIOUSLY I

    JUST WANT TO TEST A FORM SUBMISSION
  68. (ϊಠӹಠ)ϊኯᵲᴸᵲ 87% of attempts to unit test your JavaScript ends

    in a table flip. True story.
  69. Doesn’t Happen Sad truth is, this is way too common.

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

    Check for ‘test’ or ‘spec’ files Probably overestimating.
  71. Omaha Omaha’s not much better.

  72. 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?
  73. 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.
  74. 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.
  75. 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.
  76. 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.
  77. 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.
  78. 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.
  79. 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.
  80. 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.
  81. 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.
  82. 6 Things to Test Only 14 lines of code. Dense,

    in the same way a ball of yarn is dense.
  83. 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
  84. 6 Things to Fail At this point, might as well

    write integration/selenium tests. Not getting any benefit from them.
  85. 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?
  86. 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
  87. 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.
  88. Dependency Injection This idea is called DI. Probably have done

    it in Java or C#
  89. 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
  90. 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
  91. 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
  92. 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
  93. 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.
  94. 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
  95. 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.
  96. 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
  97. 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
  98. 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
  99. 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
  100. 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
  101. 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!
  102. Putting your tests to work Will have a large test

    suite. Here’s how you make it work.
  103. LiveReload

  104. 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
  105. 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
  106. CI server Trending information Email when build fails No more

    broken builds
  107. Tools Code Tips The tools you use, how it changes

    the code you write, and some tips on how to get started
  108. 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
  109. 2. Safety Net Refactoring Always get back to green

  110. 3. TDD Write failing test Write production code Forces you

    to design so it’s testable I write all my code this way
  111. 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!
  112. 5. Pair Work with someone. Especially if you’re both new

    Keeps you honest, you learn how to make it work
  113. 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.
  114. fin @mattdsteele http://matthew-steele.com Questions? Will post slides here afterwards.