Share
Embed
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
-
$(function(){ var $button = $('.submit-button'); $button.live('click', function(){ var $input =
$('input.secret-code'); }); }); -
$(function(){ var $button = $('.submit-button'); $button.live('click', function(){ var $input =
$('input.secret-code'); $.ajax({ }); }); }); -
$(function(){ var $button = $('.submit-button'); $button.live('click', function(){ var $input =
$('input.secret-code'); $.ajax({ data: $input.val() }); }); }); -
$(function(){ var $button = $('.submit-button'); $button.live('click', function(){ var $input =
$('input.secret-code'); $.ajax({ data: $input.val(), success: function(data){ } }); }); }); -
$(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>' ); } }); }); }); -
$(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>' ); } }); }); }); -
$(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>' ); } }); }); }); -
$(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 -
$(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 -
$(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 -
$(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 -
$(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 -
$(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 -
$(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 -
$(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 -
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>'); }); }); }); -
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'); }); ... -
describe("~ clicking a button ", function() { beforeEach(function(){ ... $button.trigger('click');
}); it("transmits secret code", function() { expect($.ajax).toHaveBeenCalledWith({ data: 'ZOMG!', success: jasmine.any(Function) }); }); ... -
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>'); }); }); }); -
...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 " " -
describe("app.models.Invoice", function() { var subject; beforeEach(function() { subject = app.models.Invoice({
price: 192, quantity: 30}); }); }); -
describe("app.models.Invoice", function() { var subject; beforeEach(function() { subject = app.models.Invoice({
price: 192, quantity: 30}); }); describe("#total", function() { }); }); -
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() { }); }); }); -
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); }); }); }); -
app.models.Invoice = function(attrs) { return { total: function() { return
attrs.price * attrs.quantity; } }; }; -
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) -
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); }); }); }); -
describe "app.models.Invoice", -> Given -> @subject = app.models.Invoice price: 192
quantity: 30 describe "#total", -> Then -> @subject.total() == 192 * 30 -
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) -
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" -
app.models.Invoice = (attrs) -> self = {} self.total = ->
attrs.price * attrs.quantity self -
app.models.Invoice = (attrs) -> self = {} self.total = ->
attrs.price * attrs.quantity self.formattedTotal = -> "$#{self.total() / 100.0}" self -
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" -
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" -
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 -
app.models.Invoice = (attrs) -> self = {} self.total = ->
attrs.price * attrs.quantity self.formattedTotal = -> "$#{self.total() / 100.0}" self -
app.models.Invoice = (attrs) -> self = {} self.total = ->
attrs.price * attrs.quantity self.formattedTotal = -> app.format.DollarizesCents(). dollarize(self.total()) self -
app.models.Invoice = (attrs) -> self = {} self.total = ->
attrs.price * attrs.quantity self.formattedTotal = -> app.format.DollarizesCents(). dollarize(9182128912891) self -
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" -
app.models.Invoice = (attrs) -> self = {} self.total = ->
attrs.price * attrs.quantity self.formattedTotal = -> app.format.DollarizesCents(). dollarize(9182128912891) self -
app.models.Invoice = (attrs) -> self = {} self.total = ->
attrs.price * attrs.quantity self.formattedTotal = -> app.format.DollarizesCents(). dollarize(self.total()) self -
describe "app.format.DollarizesCents", -> Given -> @subject = app.format.DollarizesCents() describe "#dollarize",
-> context "0 cents", -> Then -> @subject.dollarize(0) == "$0.00" -
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" -
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}" -
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 -
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" -
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 ""}" -
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" -
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 ""}" -
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 ""}" -
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} -
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 -
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 -
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}" -
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," -
describe "app.views.Invoice", -> Given -> @subject = app.views.Invoice() describe "#render",
-> Given -> @$el = $('<div></div>'). appendTo('body') -
describe "app.views.Invoice", -> Given -> @subject = app.views.Invoice() describe "#render",
-> Given -> @$el = $('<div></div>'). appendTo('body') When -> @subject.render(@$el) -
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') -
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') -
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') -
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') -
$button = affix('.button') #=> <div class="button"></div> $button.affix('input#firstName[value="Joe"]') #=> <div class="button">
<input id="firstName" value="Joe"/> </div> -
$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') -
$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> -
$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! -
describe "app.views.Invoice", -> Given -> @subject = app.views.Invoice() describe "#render",
-> Given -> @$el = affix('div') When -> @subject.render(@$el) Then -> expect(@$el).toContain('.total') -
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") -
app.views.Invoice = (config) -> render: ($el) -> $el.append "<div class=\"total\">"+
config.model.formattedTotal()+ "</div>" -
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") -
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") -
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") -
TDD helps us to: - think hard - respond to
pain - keep moving forward (tests are a side e ect) -
When a solution isn't obvious, defer to a new object
and fake it 'til you make it.