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

confidence.js

 confidence.js

My goal with this talk was to go beyond a simple TDD intro; instead, this is a brief tour of how I actually practice test-driven development when I'm using Jasmine and CoffeeScript.

Resources:

* http://tryjasmine.com - play with Jasmine right from your browser!
* https://github.com/searls/jasmine-fixture - easy DOM setup for tests
* https://github.com/searls/jasmine-stealth - add-ons to Jasmine's Spies API
* https://github.com/searls/jasmine-given - a Given-When-Then DSL for Jasmine
* http://searls.test-double.com/2012/04/01/types-of-tests/ - a blog post about the various types of tests, and why isolation testing is nifty

Justin Searls

May 05, 2012
Tweet

More Decks by Justin Searls

Other Decks in Programming

Transcript

  1. $(function(){ var $button = $('.submit-button'); $button.live('click', function(){ var $input =

    $('input.secret-code'); $.ajax({ data: $input.val() }); }); });
  2. $(function(){ var $button = $('.submit-button'); $button.live('click', function(){ var $input =

    $('input.secret-code'); $.ajax({ data: $input.val(), success: function(data){ } }); }); });
  3. $(function(){ var $button = $('.submit-button'); $button.live('click', function(){ var $input =

    $('input.secret-code'); $.ajax({ data: $input.val(), success: function(data){ $('.messages').append( '<div>Success! '+data.result+'</div>' ); } }); }); });
  4. $(function(){ var $button = $('.submit-button'); $button.live('click', function(){ var $input =

    $('input.secret-code'); $.ajax({ data: $input.val(), success: function(data){ $('.messages').append( '<div>Success! '+data.result+'</div>' ); } }); }); });
  5. $(function(){ var $button = $('.submit-button'); $button.live('click', function(){ var $input =

    $('input.secret-code'); $.ajax({ data: $input.val(), success: function(data){ $('.messages').append( '<div>Success! '+data.result+'</div>' ); } }); }); });
  6. $(function(){ var $button = $('.submit-button'); $button.live('click', function(){ var $input =

    $('input.secret-code'); $.ajax({ data: $input.val(), success: function(data){ $('.messages').append( '<div>Success! '+data.result+'</div>' ); } }); }); }); anonymous, not reusable
  7. $(function(){ var $button = $('.submit-button'); $button.live('click', function(){ var $input =

    $('input.secret-code'); $.ajax({ data: $input.val(), success: function(data){ $('.messages').append( '<div>Success! '+data.result+'</div>' ); } }); }); }); anonymous, not reusable handle page event
  8. $(function(){ var $button = $('.submit-button'); $button.live('click', function(){ var $input =

    $('input.secret-code'); $.ajax({ data: $input.val(), success: function(data){ $('.messages').append( '<div>Success! '+data.result+'</div>' ); } }); }); }); anonymous, not reusable handle page event user event handling
  9. $(function(){ var $button = $('.submit-button'); $button.live('click', function(){ var $input =

    $('input.secret-code'); $.ajax({ data: $input.val(), success: function(data){ $('.messages').append( '<div>Success! '+data.result+'</div>' ); } }); }); }); anonymous, not reusable handle page event user event handling sends a network request
  10. $(function(){ var $button = $('.submit-button'); $button.live('click', function(){ var $input =

    $('input.secret-code'); $.ajax({ data: $input.val(), success: function(data){ $('.messages').append( '<div>Success! '+data.result+'</div>' ); } }); }); }); anonymous, not reusable handle page event user event handling sends a network request form processing
  11. $(function(){ var $button = $('.submit-button'); $button.live('click', function(){ var $input =

    $('input.secret-code'); $.ajax({ data: $input.val(), success: function(data){ $('.messages').append( '<div>Success! '+data.result+'</div>' ); } }); }); }); anonymous, not reusable handle page event user event handling sends a network request form processing network event handling
  12. $(function(){ var $button = $('.submit-button'); $button.live('click', function(){ var $input =

    $('input.secret-code'); $.ajax({ data: $input.val(), success: function(data){ $('.messages').append( '<div>Success! '+data.result+'</div>' ); } }); }); }); anonymous, not reusable handle page event user event handling sends a network request form processing network event handling HTML templating
  13. $(function(){ var $button = $('.submit-button'); $button.live('click', function(){ var $input =

    $('input.secret-code'); $.ajax({ data: $input.val(), success: function(data){ $('.messages').append( '<div>Success! '+data.result+'</div>' ); } }); }); }); anonymous, not reusable handle page event user event handling sends a network request form processing network event handling HTML templating
  14. describe("~ clicking a button ", function() { var $button, $input,

    $messages; beforeEach(function(){ $button = affix('.submit-button'); $input = affix('input.secret-code').val('ZOMG!'); $messages = affix('.messages'); spyOn($, "ajax"); $button.trigger('click'); }); it("transmits secret code", function() { expect($.ajax).toHaveBeenCalledWith({ data: 'ZOMG!', success: jasmine.any(Function) }); }); describe("~ AJAX success", function() { beforeEach(function() { $.ajax.mostRecentCall.args[0].success({ result: "Panda!" }); }); it("appends text", function() { expect($messages).toHaveHtml( '<div>Success! Panda!</div>'); }); }); });
  15. describe("~ clicking a button ", function() { var $button, $input,

    $messages; beforeEach(function(){ $button = affix('.submit-button'); $input = affix('input.secret-code'). val('ZOMG!'); $messages = affix('.messages'); spyOn($, "ajax"); $button.trigger('click'); }); ...
  16. describe("~ clicking a button ", function() { beforeEach(function(){ ... $button.trigger('click');

    }); it("transmits secret code", function() { expect($.ajax).toHaveBeenCalledWith({ data: 'ZOMG!', success: jasmine.any(Function) }); }); ...
  17. describe("~ clicking a button ", function() { beforeEach(function(){ ... $button.trigger('click');

    }); ... describe("~ AJAX success", function() { beforeEach(function() { $.ajax.mostRecentCall.args[0].success({ result: "Panda!" }); }); it("appends text", function() { expect($messages).toHaveHtml( '<div>Success! Panda!</div>'); }); }); });
  18. ...we can't look at testing mechanistically. Unit testing does not

    improve quality just by catching errors at the unit level... The truth is more subtle than that. Quality is a function of thought and reflection - precise thought and reflection. That’s the magic. Techniques which reinforce that discipline invariably increase quality. -Michael Feathers " "
  19. describe("app.models.Invoice", function() { var subject; beforeEach(function() { subject = app.models.Invoice({

    price: 192, quantity: 30}); }); describe("#total", function() { }); });
  20. describe("app.models.Invoice", function() { var subject; beforeEach(function() { subject = app.models.Invoice({

    price: 192, quantity: 30}); }); describe("#total", function() { it("is price x quantity", function() { }); }); });
  21. describe("app.models.Invoice", function() { var subject; beforeEach(function() { subject = app.models.Invoice({

    price: 192, quantity: 30}); }); describe("#total", function() { it("is price x quantity", function() { expect(subject.total()).toEqual(5760); }); }); });
  22. describe "app.models.Invoice", -> beforeEach -> @subject = app.models.Invoice price: 192

    quantity: 30 describe "#total", -> it "is price x quantity", -> expect(@subject.total()).toEqual(5760)
  23. describe("app.models.Invoice", function() { var subject; beforeEach(function() { subject = app.models.Invoice({

    price: 192, quantity: 30}); }); describe("#total", function() { it("is price x quantity", function() { expect(subject.total()).toEqual(5760); }); }); });
  24. describe "app.models.Invoice", -> Given -> @subject = app.models.Invoice price: 192

    quantity: 30 describe "#total", -> Then -> @subject.total() == 192 * 30
  25. describe "app.models.Invoice", -> beforeEach -> @subject = app.models.Invoice price: 192

    quantity: 30 describe "#total", -> it "is price x quantity", -> expect(@subject.total()).toEqual(5760)
  26. describe "app.models.Invoice", -> Given -> @subject = app.models.Invoice price: 192

    quantity: 30 describe "#total", -> Then -> @subject.total() == 192 * 30 describe "#formattedTotal", -> When -> @result = @subject.formattedTotal() Then -> @result == "$57.60"
  27. app.models.Invoice = (attrs) -> self = {} self.total = ->

    attrs.price * attrs.quantity self.formattedTotal = -> "$#{self.total() / 100.0}" self
  28. describe "app.models.Invoice", -> Given -> @subject = app.models.Invoice price: 192

    quantity: 30 describe "#total", -> Then -> @subject.total() == 192 * 30 describe "#formattedTotal", -> When -> @result = @subject.formattedTotal() Then -> @result == "$57.60"
  29. describe "app.models.Invoice", -> Given -> @subject = app.models.Invoice price: 192

    quantity: 30 describe "#total", -> Then -> @subject.total() == 192 * 30 describe "#formattedTotal", -> Given -> spyOn(app.format, "DollarizesCents").andReturn dollarize: -> "$57.60" When -> @result = @subject.formattedTotal() Then -> @result == "$57.60"
  30. spyOn(app.format, "DollarizesCents").andReturn dollarize: -> "$57.60" #1. Store the real app.format.DollarizesCents

    #2. Set app.format.DollarizesCents to a fake function (called a spy). #3. Tell that spy function to return some object with a dollarize method that returns the test data "$57.60" #4. After the spec runs, replace the original app.format.DollarizesCents function
  31. app.models.Invoice = (attrs) -> self = {} self.total = ->

    attrs.price * attrs.quantity self.formattedTotal = -> "$#{self.total() / 100.0}" self
  32. app.models.Invoice = (attrs) -> self = {} self.total = ->

    attrs.price * attrs.quantity self.formattedTotal = -> app.format.DollarizesCents(). dollarize(self.total()) self
  33. app.models.Invoice = (attrs) -> self = {} self.total = ->

    attrs.price * attrs.quantity self.formattedTotal = -> app.format.DollarizesCents(). dollarize(9182128912891) self
  34. describe "app.models.Invoice", -> Given -> @subject = app.models.Invoice price: 192

    quantity: 30 describe "#total", -> Then -> @subject.total() == 192 * 30 describe "#formattedTotal", -> Given -> spyOn(app.format, "DollarizesCents").andReturn dollarize: jasmine.createSpy().when(5760).thenReturn("$57.60") When -> @result = @subject.formattedTotal() Then -> @result == "$57.60"
  35. app.models.Invoice = (attrs) -> self = {} self.total = ->

    attrs.price * attrs.quantity self.formattedTotal = -> app.format.DollarizesCents(). dollarize(9182128912891) self
  36. app.models.Invoice = (attrs) -> self = {} self.total = ->

    attrs.price * attrs.quantity self.formattedTotal = -> app.format.DollarizesCents(). dollarize(self.total()) self
  37. describe "app.format.DollarizesCents", -> Given -> @subject = app.format.DollarizesCents() describe "#dollarize",

    -> context "0 cents", -> Then -> @subject.dollarize(0) == "$0.00" context "50 cents", -> Then -> @subject.dollarize(50) == "$0.50"
  38. app.format.DollarizesCents = -> dollarize: (cents) -> dollars = cents /

    100.0 decimal = if dollars % 1 == 0 then "." else "" trailingZero = if cents % 10 == 0 then "0" else "" trailingDoubleZero = if cents % 100 == 0 then "0" else "" "$#{dollars}#{decimal}#{trailingZero}#{trailingDoubleZero}"
  39. app.format.DollarizesCents = -> dollarize: (cents) -> s = "$" s

    += (dollars = cents / 100.0) s += "." if dollars % 1 == 0 s += "0" if cents % 10 == 0 s += "0" if cents % 100 == 0 s
  40. describe "app.format.DollarizesCents", -> Given -> @subject = app.format.DollarizesCents() describe "#dollarize",

    -> context "0 cents", -> Then -> @subject.dollarize(0) == "$0.00" context "50 cents", -> Then -> @subject.dollarize(50) == "$0.50" context "1,841,482 cents", -> Then -> @subject.dollarize(1841482) == "$18,414.82"
  41. app.format.DollarizesCents = -> dollarize: (cents) -> amount = cents /

    100.0 s = "$" s += @commasFor(amount) s += "0" if cents % 10 == 0 s += "0" if cents % 100 == 0 s #private commasFor: (amount) -> [dollars, cents] = amount.toString().split(".") s = "" while dollars.length > 3 s = ",#{dollars.slice(-3)}#{s}" dollars = dollars.substring(0, dollars.length - 3) "#{dollars}#{s}.#{cents or ""}"
  42. describe "app.format.DollarizesCents", -> Given -> @subject = app.format.DollarizesCents() describe "#dollarize",

    -> context "0 cents", -> Then -> @subject.dollarize(0) == "$0.00" context "50 cents", -> Then -> @subject.dollarize(50) == "$0.50" context "1,841,482 cents", -> Then -> @subject.dollarize(1841482) == "$18,414.82" context "213,981,400 cents", -> Then -> @subject.dollarize(213981400) == "$2,139,814.00"
  43. app.format.DollarizesCents = -> dollarize: (cents) -> amount = cents /

    100.0 s = "$" s += @commasFor(amount) s += "0" if cents % 10 == 0 s += "0" if cents % 100 == 0 s #private commasFor: (amount) -> [dollars, cents] = amount.toString().split(".") s = "" while dollars.length > 3 s = ",#{dollars.slice(-3)}#{s}" dollars = dollars.substring(0, dollars.length - 3) "#{dollars}#{s}.#{cents or ""}"
  44. app.format.DollarizesCents = -> dollarize: (cents) -> amount = cents /

    100.0 s = "$" s += @commasFor(amount) s += "0" if cents % 10 == 0 s += "0" if cents % 100 == 0 s #private commasFor: (amount) -> [dollars, cents] = amount.toString().split(".") s = "" while dollars.length > 3 s = ",#{dollars.slice(-3)}#{s}" dollars = dollars.substring(0, dollars.length - 3) "#{dollars}#{s}.#{cents or ""}"
  45. app.format.DollarizesCents = -> dollarize: (pennies) -> amount = pennies /

    100.0 [dollars, cents] = amount.toString().split(".") s = "$" s += @commasFor(dollars) s += ".#{cents or ""}" s += "0" if pennies % 10 == 0 s += "0" if pennies % 100 == 0 s #private commasFor: (dollars) -> s = "" while dollars.length > 3 s = ",#{dollars.slice(-3)}#{s}" dollars = dollars.substring(0, dollars.length - 3) "#{dollars}#{s}
  46. app.format.DollarizesCents = -> dollarize: (pennies) -> amount = pennies /

    100.0 [dollars, cents] = amount.toString().split(".") s = "$" s += @commasFor(dollars) s += "." s += @paddingFor(cents) s #private commasFor: (dollars) -> s = "" while dollars.length > 3 s = ",#{dollars.slice(-3)}#{s}" dollars = dollars.substring(0, dollars.length - 3) "#{dollars}#{s}" paddingFor: (cents = "") -> while cents.length < 2 cents += "0" cents
  47. app.format.DollarizesCents = -> dollarize: (pennies) -> amount = pennies /

    100.0 [dollars, cents] = amount.toString().split(".") "$#{@commasFor(dollars)}.#{@paddingFor(cents)}" #private commasFor: (dollars) -> s = "" while dollars.length > 3 s = ",#{dollars.slice(-3)}#{s}" dollars = dollars.substring(0, dollars.length - 3) "#{dollars}#{s}" paddingFor: (cents = "") -> while cents.length < 2 cents += "0" cents
  48. app.format.DollarizesCents = -> dollarize: (pennies) -> amount = (pennies /

    100.0).toFixed(2) [dollars, cents] = amount.toString().split(".") "$#{@commasFor(dollars)}.#{cents}" #private commasFor: (dollars) -> s = "" while dollars.length > 3 s = ",#{dollars.slice(-3)}#{s}" dollars = dollars.substring(0, dollars.length - 3) "#{dollars}#{s}"
  49. app.format.DollarizesCents = -> dollarize: (pennies) -> amount = (pennies /

    100.0).toFixed(2) [dollars, cents] = amount.toString().split(".") "$#{@commasFor(dollars)}.#{cents}" #private commasFor: (dollars) -> dollars.replace /(\d)(?=(\d{3})+$)/g, "\$1,"
  50. describe "app.views.Invoice", -> Given -> @subject = app.views.Invoice() describe "#render",

    -> Given -> @$el = $('<div></div>'). appendTo('body') When -> @subject.render(@$el)
  51. describe "app.views.Invoice", -> Given -> @subject = app.views.Invoice() describe "#render",

    -> Given -> @$el = $('<div></div>'). appendTo('body') When -> @subject.render(@$el) Then -> expect(@$el).toContain('.total')
  52. describe "app.views.Invoice", -> Given -> @model = app.models.Invoice() Given ->

    @subject = app.views.Invoice model: @model describe "#render", -> Given -> @$el = $('<div></div>'). appendTo('body') When -> @subject.render(@$el) Then -> expect(@$el).toContain('.total')
  53. describe "app.views.Invoice", -> Given -> @model = app.models.Invoice() Given ->

    @subject = app.views.Invoice model: @model describe "#render", -> Given -> @$el = $('<div></div>'). appendTo('body') afterEach -> @$el.remove() When -> @subject.render(@$el) Then -> expect(@$el).toContain('.total')
  54. describe "app.views.Invoice", -> Given -> @model = app.models.Invoice() Given ->

    @subject = app.views.Invoice model: @model describe "#render", -> Given -> @$el = affix('div') When -> @subject.render(@$el) Then -> expect(@$el).toContain('.total')
  55. $button = affix('.button') #=> <div class="button"></div> $button.affix('input#firstName[value="Joe"]') #=> <div class="button">

    <input id="firstName" value="Joe"/> </div> affix('pre code div.example') #=> <pre><code><div class="example"></div></code></pre>
  56. $button = affix('.button') #=> <div class="button"></div> $button.affix('input#firstName[value="Joe"]') #=> <div class="button">

    <input id="firstName" value="Joe"/> </div> affix('pre code div.example') #=> <pre><code><div class="example"></div></code></pre> # And it deletes affixed elements afterEach spec!
  57. describe "app.views.Invoice", -> Given -> @subject = app.views.Invoice() describe "#render",

    -> Given -> @$el = affix('div') When -> @subject.render(@$el) Then -> expect(@$el).toContain('.total')
  58. describe "app.views.Invoice", -> Given -> @model = app.models.Invoice price: 38

    quantity: 2 Given -> @subject = app.views.Invoice model: @model describe "#render", -> Given -> @$el = affix('div') When -> @subject.render(@$el) Then -> expect(@$el.find('.total')). toHaveText("$0.76")
  59. describe "app.views.Invoice", -> Given -> @model = app.models.Invoice price: 38

    quantity: 2 Given -> @subject = app.views.Invoice model: @model describe "#render", -> Given -> @$el = affix('div') When -> @subject.render(@$el) Then -> expect(@$el).toContain('.total') Then -> expect(@$el.find('.total')). toHaveText("$0.76")
  60. describe "app.views.Invoice", -> Given -> @model = {} Given ->

    @subject = app.views.Invoice model: @model describe "#render", -> Given -> @$el = affix('div') When -> @subject.render(@$el) Then -> expect(@$el).toContain('.total') Then -> expect(@$el.find('.total')). toHaveText("$0.76")
  61. describe "app.views.Invoice", -> Given -> @model = {} Given ->

    @subject = app.views.Invoice model: @model describe "#render", -> Given -> @model.formattedTotal = jasmine.createSpy().andReturn("$foo") Given -> @$el = affix('div') When -> @subject.render(@$el) Then -> expect(@$el).toContain('.total') Then -> expect(@$el.find('.total')). toHaveText("$foo")
  62. TIL

  63. TDD helps us to: - think hard - respond to

    pain - keep moving forward (tests are a side e ect)