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

Transcript

156 slides
  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)

More Decks by Justin Searls

Other Decks in Programming