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

E6c6e133e74c3b83f04d2861deaa1c20?s=128

Justin Searls

May 05, 2012
Tweet

Transcript

  1. confidence.js

  2. "I don't test-drive my JavaScript because it changes too often"

    - Anonymous
  3. Why does it change more often?

  4. Because it has more reasons to!

  5. $(function(){ });

  6. $(function(){ var $button = $('.submit-button'); });

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

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

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

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

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

    $('input.secret-code'); $.ajax({ data: $input.val(), success: function(data){ } }); }); });
  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>' ); } }); }); });
  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>' ); } }); }); });
  14. simple!

  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>' ); } }); }); });
  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
  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
  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
  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
  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
  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
  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
  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
  24. SRP, or...

  25. SR—WHEE!

  26. information density

  27. can we test that code as-is?

  28. None
  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>'); }); }); });
  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'); }); ...
  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) }); }); ...
  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>'); }); }); });
  33. None
  34. Testing vs. TDD

  35. let's talk feedback

  36. What can a full-stack test tell us?

  37. 1. A feature is implemented

  38. 2. The application seems to be working

  39. 3. We didn't just break everything

  40. What can a unit test tell us?

  41. 1. How the unit behaves

  42. 2. How the unit depends on other units

  43. 3. How great/awful the unit's API is

  44. TDD isn't about catching bugs

  45. TDD isn't about preventing bugs

  46. TDD isn't about testing

  47. TDD is about thinking harder

  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 " "
  49. TDD is about responding to painful designs

  50. TDD is about discovering what we need

  51. TDD is not the only way

  52. let's try again

  53. our serious app has invoices

  54. describe("app.models.Invoice", function() { });

  55. describe("app.models.Invoice", function() { var subject; });

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

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

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

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

    price: 192, quantity: 30}); }); describe("#total", function() { }); });
  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() { }); }); });
  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); }); }); });
  62. app.models.Invoice = function(attrs) { return { total: function() { return

    attrs.price * attrs.quantity; } }; };
  63. Now, in Co eeVision

  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)
  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); }); }); });
  66. now, in Given-When-Then w/ jasmine-given

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

    quantity: 30 describe "#total", -> Then -> @subject.total() == 192 * 30
  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)
  69. let's inflict some pain

  70. formatting dollars

  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"
  72. app.models.Invoice = (attrs) -> self = {} self.total = ->

    attrs.price * attrs.quantity self
  73. app.models.Invoice = (attrs) -> self = {} self.total = ->

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

  75. hrm.

  76. (punting on) formatting dollars

  77. Defer, Defer, Defer!

  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"
  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"
  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
  81. spyOn(obj, "methodName")

  82. jasmine.createSpy("blah")

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

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

    attrs.price * attrs.quantity self.formattedTotal = -> app.format.DollarizesCents(). dollarize(self.total()) self
  85. fake it 'til you make it

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

    attrs.price * attrs.quantity self.formattedTotal = -> app.format.DollarizesCents(). dollarize(9182128912891) self
  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"
  89. app.models.Invoice = (attrs) -> self = {} self.total = ->

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

    attrs.price * attrs.quantity self.formattedTotal = -> app.format.DollarizesCents(). dollarize(self.total()) self
  91. when().thenReturn() & other spy gadgets are in jasmine-stealth

  92. (actually) formatting dollars

  93. describe "app.format.DollarizesCents", -> Given -> @subject = app.format.DollarizesCents() describe "#dollarize",

    -> context "0 cents", -> Then -> @subject.dollarize(0) == "$0.00"
  94. app.format.DollarizesCents = -> dollarize: (cents) -> "$0.00"

  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"
  96. Crap.

  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}"
  98. Yuck!

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

  101. Let's lean into it.

  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"
  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 ""}"
  104. I am now embarrassed.

  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"
  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 ""}"
  107. commasFor is doing too much

  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 ""}"
  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}
  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
  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
  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}"
  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,"
  114. what about the DOM?

  115. I want my asp/jsp/erb!

  116. No you don't.

  117. I want to load HTML fixture files!

  118. No you don't.

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

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

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

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

    -> Given -> @$el = $('<div></div>'). appendTo('body') When -> @subject.render(@$el)
  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')
  124. app.views.Invoice = () -> render: ($el) -> $el.append("<div class=\"total\"></div>")

  125. test pollution

  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')
  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')
  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')
  129. jasmine-fixture a x()

  130. it's jQuery in reverse!

  131. None
  132. $button = affix('.button')

  133. $button = affix('.button') #=> <div class="button"></div>

  134. $button = affix('.button') #=> <div class="button"></div> $button.affix('input#firstName[value="Joe"]')

  135. $button = affix('.button') #=> <div class="button"></div> $button.affix('input#firstName[value="Joe"]') #=> <div class="button">

    <input id="firstName" value="Joe"/> </div>
  136. $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')
  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>
  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!
  139. Now, formatted price

  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')
  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")
  142. app.views.Invoice = (config) -> render: ($el) -> $el.append "<div class=\"total\">"+

    config.model.formattedTotal()+ "</div>"
  143. spec leakage

  144. the view spec knows how the model's formattedTotal works

  145. that's not very DRY

  146. collaboration > implementation

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

  151. If you're not open to changing your code's design, TDD

    won't help.
  152. TDD helps us to: - think hard - respond to

    pain - keep moving forward (tests are a side e ect)
  153. When a solution isn't obvious, defer to a new object

    and fake it 'til you make it.
  154. Shared factories & fixtures lead to a Tragedy of the

    Commons
  155. Isolate subjects from collaborators because it'll hurt so good.

  156. http://test-double.com @searls http://github.com/searls http://tryjasmine.com