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

E6c6e133e74c3b83f04d2861deaa1c20?s=128

Justin Searls

May 05, 2012
Tweet

Transcript

  1. 10.

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

    $('input.secret-code'); $.ajax({ data: $input.val() }); }); });
  2. 11.

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

    $('input.secret-code'); $.ajax({ data: $input.val(), success: function(data){ } }); }); });
  3. 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>' ); } }); }); });
  4. 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>' ); } }); }); });
  5. 14.
  6. 15.

    $(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>' ); } }); }); });
  7. 16.

    $(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
  8. 17.

    $(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
  9. 18.

    $(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
  10. 19.

    $(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
  11. 20.

    $(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
  12. 21.

    $(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
  13. 22.

    $(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. 23.

    $(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
  15. 28.
  16. 29.

    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>'); }); }); });
  17. 30.

    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'); }); ...
  18. 31.

    describe("~ clicking a button ", function() { beforeEach(function(){ ... $button.trigger('click');

    }); it("transmits secret code", function() { expect($.ajax).toHaveBeenCalledWith({ data: 'ZOMG!', success: jasmine.any(Function) }); }); ...
  19. 32.

    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>'); }); }); });
  20. 33.
  21. 48.

    ...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 " "
  22. 59.

    describe("app.models.Invoice", function() { var subject; beforeEach(function() { subject = app.models.Invoice({

    price: 192, quantity: 30}); }); describe("#total", function() { }); });
  23. 60.

    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() { }); }); });
  24. 61.

    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); }); }); });
  25. 64.

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

    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); }); }); });
  27. 67.

    describe "app.models.Invoice", -> Given -> @subject = app.models.Invoice price: 192

    quantity: 30 describe "#total", -> Then -> @subject.total() == 192 * 30
  28. 68.

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

    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"
  30. 72.
  31. 73.

    app.models.Invoice = (attrs) -> self = {} self.total = ->

    attrs.price * attrs.quantity self.formattedTotal = -> "$#{self.total() / 100.0}" self
  32. 74.
  33. 75.
  34. 78.

    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"
  35. 79.

    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"
  36. 80.

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

    app.models.Invoice = (attrs) -> self = {} self.total = ->

    attrs.price * attrs.quantity self.formattedTotal = -> "$#{self.total() / 100.0}" self
  38. 84.

    app.models.Invoice = (attrs) -> self = {} self.total = ->

    attrs.price * attrs.quantity self.formattedTotal = -> app.format.DollarizesCents(). dollarize(self.total()) self
  39. 86.
  40. 87.

    app.models.Invoice = (attrs) -> self = {} self.total = ->

    attrs.price * attrs.quantity self.formattedTotal = -> app.format.DollarizesCents(). dollarize(9182128912891) self
  41. 88.

    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"
  42. 89.

    app.models.Invoice = (attrs) -> self = {} self.total = ->

    attrs.price * attrs.quantity self.formattedTotal = -> app.format.DollarizesCents(). dollarize(9182128912891) self
  43. 90.

    app.models.Invoice = (attrs) -> self = {} self.total = ->

    attrs.price * attrs.quantity self.formattedTotal = -> app.format.DollarizesCents(). dollarize(self.total()) self
  44. 95.

    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"
  45. 96.
  46. 97.

    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}"
  47. 98.
  48. 99.

    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
  49. 100.
  50. 102.

    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"
  51. 103.

    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 ""}"
  52. 105.

    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"
  53. 106.

    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 ""}"
  54. 108.

    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 ""}"
  55. 109.

    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}
  56. 110.

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

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

    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}"
  59. 113.

    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,"
  60. 122.

    describe "app.views.Invoice", -> Given -> @subject = app.views.Invoice() describe "#render",

    -> Given -> @$el = $('<div></div>'). appendTo('body') When -> @subject.render(@$el)
  61. 123.

    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')
  62. 126.

    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')
  63. 127.

    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')
  64. 128.

    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')
  65. 131.
  66. 137.

    $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>
  67. 138.

    $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!
  68. 140.

    describe "app.views.Invoice", -> Given -> @subject = app.views.Invoice() describe "#render",

    -> Given -> @$el = affix('div') When -> @subject.render(@$el) Then -> expect(@$el).toContain('.total')
  69. 141.

    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")
  70. 147.

    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")
  71. 148.

    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")
  72. 149.

    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")
  73. 150.

    TIL

  74. 152.

    TDD helps us to: - think hard - respond to

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