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. confidence.js

    View full-size slide

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

    View full-size slide

  3. Why does it
    change
    more often?

    View full-size slide

  4. Because it
    has more
    reasons to!

    View full-size slide

  5. $(function(){
    });

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  23. information
    density

    View full-size slide

  24. can we test that
    code as-is?

    View full-size slide

  25. 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(
    'Success! Panda!');
    });
    });
    });

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  28. 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(
    'Success! Panda!');
    });
    });
    });

    View full-size slide

  29. Testing
    vs.
    TDD

    View full-size slide

  30. let's talk
    feedback

    View full-size slide

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

    View full-size slide

  32. 1. A feature is
    implemented

    View full-size slide

  33. 2. The application
    seems to be
    working

    View full-size slide

  34. 3. We didn't just break
    everything

    View full-size slide

  35. What can a
    unit test
    tell us?

    View full-size slide

  36. 1. How the unit
    behaves

    View full-size slide

  37. 2. How the unit
    depends on other
    units

    View full-size slide

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

    View full-size slide

  39. TDD isn't
    about
    catching bugs

    View full-size slide

  40. TDD isn't
    about
    preventing bugs

    View full-size slide

  41. TDD isn't
    about
    testing

    View full-size slide

  42. TDD is about
    thinking
    harder

    View full-size slide

  43. ...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
    "
    "

    View full-size slide

  44. TDD is about
    responding to
    painful designs

    View full-size slide

  45. TDD is about
    discovering
    what we need

    View full-size slide

  46. TDD is not
    the only way

    View full-size slide

  47. let's try again

    View full-size slide

  48. our serious
    app has invoices

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  57. app.models.Invoice = function(attrs) {
    return {
    total: function() {
    return attrs.price * attrs.quantity;
    }
    };
    };

    View full-size slide

  58. Now, in
    Co eeVision

    View full-size slide

  59. 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)

    View full-size slide

  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() {
    expect(subject.total()).toEqual(5760);
    });
    });
    });

    View full-size slide

  61. now, in
    Given-When-Then
    w/ jasmine-given

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  64. let's inflict
    some pain

    View full-size slide

  65. formatting
    dollars

    View full-size slide

  66. 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"

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  69. (punting on)
    formatting
    dollars

    View full-size slide

  70. Defer,
    Defer,
    Defer!

    View full-size slide

  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"

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  74. spyOn(obj, "methodName")

    View full-size slide

  75. jasmine.createSpy("blah")

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  78. fake it 'til you
    make it

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  83. when().thenReturn()
    & other spy gadgets are in
    jasmine-stealth

    View full-size slide

  84. (actually)
    formatting
    dollars

    View full-size slide

  85. describe "app.format.DollarizesCents", ->
    Given -> @subject = app.format.DollarizesCents()
    describe "#dollarize", ->
    context "0 cents", ->
    Then -> @subject.dollarize(0) == "$0.00"

    View full-size slide

  86. app.format.DollarizesCents = ->
    dollarize: (cents) -> "$0.00"

    View full-size slide

  87. 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"

    View full-size slide

  88. 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}"

    View full-size slide

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

    View full-size slide

  90. Let's lean into it.

    View full-size slide

  91. 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"

    View full-size slide

  92. 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 ""}"

    View full-size slide

  93. I am now
    embarrassed.

    View full-size slide

  94. 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"

    View full-size slide

  95. 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 ""}"

    View full-size slide

  96. commasFor is
    doing too much

    View full-size slide

  97. 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 ""}"

    View full-size slide

  98. 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}

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  101. 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}"

    View full-size slide

  102. 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,"

    View full-size slide

  103. what about
    the DOM?

    View full-size slide

  104. I want my
    asp/jsp/erb!

    View full-size slide

  105. No you don't.

    View full-size slide

  106. I want to load
    HTML fixture
    files!

    View full-size slide

  107. No you don't.

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  112. describe "app.views.Invoice", ->
    Given -> @subject = app.views.Invoice()
    describe "#render", ->
    Given -> @$el = $('').
    appendTo('body')
    When -> @subject.render(@$el)
    Then -> expect(@$el).toContain('.total')

    View full-size slide

  113. app.views.Invoice = () ->
    render: ($el) ->
    $el.append("")

    View full-size slide

  114. test pollution

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  117. 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')

    View full-size slide

  118. jasmine-fixture
    a x()

    View full-size slide

  119. it's jQuery
    in reverse!

    View full-size slide

  120. $button = affix('.button')

    View full-size slide

  121. $button = affix('.button')
    #=>

    View full-size slide

  122. $button = affix('.button')
    #=>
    $button.affix('input#firstName[value="Joe"]')

    View full-size slide

  123. $button = affix('.button')
    #=>
    $button.affix('input#firstName[value="Joe"]')
    #=>


    View full-size slide

  124. $button = affix('.button')
    #=>
    $button.affix('input#firstName[value="Joe"]')
    #=>


    affix('pre code div.example')

    View full-size slide

  125. $button = affix('.button')
    #=>
    $button.affix('input#firstName[value="Joe"]')
    #=>


    affix('pre code div.example')
    #=>

    View full-size slide

  126. $button = affix('.button')
    #=>
    $button.affix('input#firstName[value="Joe"]')
    #=>


    affix('pre code div.example')
    #=>
    # And it deletes affixed elements afterEach spec!

    View full-size slide

  127. Now,
    formatted
    price

    View full-size slide

  128. describe "app.views.Invoice", ->
    Given -> @subject = app.views.Invoice()
    describe "#render", ->
    Given -> @$el = affix('div')
    When -> @subject.render(@$el)
    Then -> expect(@$el).toContain('.total')

    View full-size slide

  129. 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")

    View full-size slide

  130. app.views.Invoice = (config) ->
    render: ($el) ->
    $el.append ""+
    config.model.formattedTotal()+
    ""

    View full-size slide

  131. spec leakage

    View full-size slide

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

    View full-size slide

  133. that's not
    very DRY

    View full-size slide

  134. collaboration
    >
    implementation

    View full-size slide

  135. 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")

    View full-size slide

  136. 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")

    View full-size slide

  137. 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")

    View full-size slide

  138. If you're not open to
    changing your code's
    design, TDD won't help.

    View full-size slide

  139. TDD helps us to:
    - think hard
    - respond to pain
    - keep moving forward
    (tests are a side e ect)

    View full-size slide

  140. When a solution isn't
    obvious, defer to a new
    object and fake it 'til
    you make it.

    View full-size slide

  141. Shared factories &
    fixtures lead to a
    Tragedy of the Commons

    View full-size slide

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

    View full-size slide

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

    View full-size slide