Test Driven Development

makandra GmbH

"Cat Burglar" by Amber Grundy

http://www.flickr.com/photos/abear23/1444321123/

makandra.com
makandra.de

We are looking for new colleagues!

Test Driven Development

What is TDD?

(with pretty pictures)

Tests capture requirements in the form of examples

Requirement

As a customer
in order to gather the products I want to purchase
I want to add items to my shopping cart

Example

Scenario: add an item to the shopping cart
  Given an article with the name "Nintendo Wii"
  When I am on the homepage
  And I follow "Nintendo Wii"
  And I press "Add to cart"
  Then I should see "Item was added to cart"
Automated tests are executable examples
Scenario: add an item to the shopping cart
  Given an article with the name "Nintendo Wii"
  When I am on the homepage
  And I follow "Nintendo Wii"
  And I press "Add to cart"
  Then I should see "Item was added to cart"
 
 
 
Scenario: add an item to the shopping cart
  Given an article with the name "Nintendo Wii"
  When I am on the homepage
  And I follow "Nintendo Wii"
  And I press "Add to cart"
  Then I should see "Item was added to cart"

Unknown class "Article"
1 scenario (1 failed)
class Article
  attr_accessor :name
end
Scenario: add an item to the shopping cart
  Given an article with the name "Nintendo Wii"
  When I am on the homepage
  And I follow "Nintendo Wii"
  And I press "Add to cart"
  Then I should see "Item was added to cart"

Error retrieving page (404 not found)
1 scenario (1 failed)
Scenario: add an item to the shopping cart
  Given an article with the name "Nintendo Wii"
  When I am on the homepage
  And I follow "Nintendo Wii"
  And I press "Add to cart"
  Then I should see "Item was added to cart"

No such link "Nintendo Wii"
1 scenario (1 failed)
Scenario: add an item to the shopping cart
  Given an article with the name "Nintendo Wii"
  When I am on the homepage
  And I follow "Nintendo Wii"
  And I press "Add to cart"
  Then I should see "Item was added to cart"

No such button "Add to cart"
1 scenario (1 failed)
Scenario: add an item to the shopping cart
  Given an article with the name "Nintendo Wii"
  When I am on the homepage
  And I follow "Nintendo Wii"
  And I press "Add to cart"
  Then I should see "Item was added to cart"

Expected to see "Item was added to cart"
1 scenario (1 failed)
Scenario: add an item to the shopping cart
  Given an article with the name "Nintendo Wii"
  When I am on the homepage
  And I follow "Nintendo Wii"
  And I press "Add to cart"
  Then I should see "Item was added to cart"

1 scenario (1 passed)
 

What just happened?

Why is this awesome?

(The big picture)

Dealing with change

writing software != building bridges

requirements evolve during implementation
defending against change
is solving the wrong problem
(I'm sorry)
dealing with change
is part of your job!
(given that requirements are always evolving)
how can I make changes
without breaking previous changes?

View the rest of this chapter here:
why-expanded.with-fonts.pdf

How can I test my applications?

(with scary code)

Making applications testable always involves the same challenges:

We have the technology!

7 hacks to get you started

Hack #1

Choose the right kind of test

Full-stack integration test

Scenario: add an item to the shopping cart
  Given an article with the name "Nintendo Wii"
  When I am on the homepage
  And I follow "Nintendo Wii"
  And I press "Add to cart"
  Then I should see "Item was added to cart"

Unit test

describe Hero do
  describe '#attack' do
    it "should damage the target for the hero's strength" do
      monster = Monster.create(:hitpoints => 100)
      hero = Hero.create(:strength => 70)
      hero.attack(monster)
      monster.hitpoints.should == 30
    end
  end
end

Hack #2

Script UI interactions

Headless browsers

Tools to script real browsers

Scripting APIs can be consumed from your test code

visit '/login'
fill_in 'E-mail', :with => 'user@example.com'
fill_in 'Password', :with => 'secret'
click_link 'Sign in'
page.should have_content('Signed in successfully')

Hack #3

Use a separate database for running tests

Cloning a database schema

mysqldump -uroot -psecret -d hero_development |
mysql -uroot -psecret hero_test

Hack #4

Isolate your tests from each other

Transactions can help you clean up

  1. 1. Clone the development database
  2. 2. Start a transaction
  3. 3. Run test #1
  4. 4. Rollback the transaction
  5. 5. Start a transaction
  6. 6. Run test #2
  7. 7. Rollback the transaction
  8. ...

Hack #5

Create test scenarios effectively




"The RSS feed should include the latest forum comment"

This is horrible:

describe Feed, '.to_rss' do
  it "should include the latest forum comment" do
    forum = Forum.create(:name => "Example Forum")
    user = User.create(:email => "foo@bar.com",
                       :name => "Hans Dampf")
    topic = Topic.create(:forum => forum,
                         :subject => "Example Topic",
                         :author => user)
    Comment.create(:topic => topic,
                   :author => user,
                   :text => "Hi world!")
    Feed.to_rss.should include("Hi world!")
  end
end
User.blueprint do
  name "John Doe"
  email "john@doe.com"
  password 'secret'
end

Forum.blueprint do
  name "Example Forum"
end

Topic.blueprint do
  forum
  subject "Example Topic"
end

Comment.blueprint do
  topic
  author
  text "Lorem ipsum dolor sit amet"
end

This is expressive:

describe Feed, '.to_rss' do
  it "should include the latest forum comment" do
    Comment.make(:text => "Hi world!")
    Feed.to_rss.should include("Hi world!")
  end
end

Hack #6

Mute side effects with stubs

PayPal.capture_payment("RE-23400123")

GoogleMaps.fetch_geocode("Augsburg")

NuclearMissile.new.launch

system("rm -rf /")
class Order

  def initialize(number)
    @number = number
    @paid = false
  end

  def finish
    if PayPal.capture_payment(@number)
      @paid = true
    end
  end

  def paid?
    @paid
  end

end
describe Order, '#finish' do
  it "should mark the order as paid" do
    order = Order.new("ON-5512")
    order.finish
    order.should be_paid
  end
end

Order finish should mark the order as paid FAILED   
 
 
 
describe Order, '#finish' do
  it "should mark the order as paid" do
    order = Order.new("ON-5512")
    order.finish
    order.should be_paid
  end
end

Order finish should mark the order as paid FAILED   
PayPalError: Unknown transaction reference "ON-5512"

1 example, 1 failure
describe Order, '#finish' do
  it "should mark the order as paid" do
    order = Order.new("ON-5512")
    order.finish
    order.should be_paid
  end
end

Order finish should mark the order as paid FAILED   
HTTPError: Could not resolve host api.paypal.com

1 example, 1 failure
Use stubbing to replace method implementations
with scripted behavior.
describe Order, '#finish' do
  it "should mark the order as paid" do
    PayPal.stub(:capture_payment).and_return(true)
    order = Order.new("ON-5512")
    order.finish
    order.should be_paid
  end
end

1 example, 0 failures
Order finish should mark the order as paid FAILED   
 

Hack #7

Observe side effects using mocks

describe Order, 'finish' do
  it "should capture the payment through PayPal" do
    # ???
  end
end
describe Order, 'finish' do
  it "should capture the payment through PayPal" do
    PayPal
      .should_receive(:capture_payment)
      .with("ON-5512")
      .and_return(true)
    order = Order.new("ON-5512")
    order.finish
  end
end

Getting started

What is TDD?

Why should I care?

How does it work?

end

Got interested to work with the good guys?
Talk to me!