BDD - Beer Driven Development

Test driven development that doesn't suck

Matthew Bennett-Lovesey / @undecisive

Quick show of hands!

  • Who has heard of test driven development before? (hopefully most of you)
  • Who has done test driven development before? (a.k.a "show-offs")
  • Who has done behaviour driven development before? (I hope I've got all the definitions right then)

In my rucksack I have got...

  • What is TDD / BDD?
    • Why would I bother?
    • When should I bother?
  • How do I set up a testing environment?
  • How do I write tests
  • How do I make sure that the tests aren't as much of an unmaintainable bohemouth as the code?

What is TDD?

Test driven development is the art of writing test code that you run that runs a bit of your production code to ensure that your production code does what you thought your production code would do when you were writing the test code.
... before that bit of production code even exists.

What is TDD?

Writing code that expresses an intention
... then making that intention a reality.

Two levels of TDD

Integration tests
Unit tests

What is Behaviour Driven Development then?

Several theories:

  • Reaction against stupid tests, aka "TDD done right"
  • A way to capture user requirements
  • A form of mind control
  • RSpec and Cucumber

Why bother?

Reasons you should (in any language):

Working on a large project Learning a new language Working with a team Frequently changing requirements Code will need to change hands Learning TDD(!)

When should you not bother?

Reasons you might avoid TDD:

When you don't care about the code When nobody else will ever see the code Job security / intentional obfuscation You're doing something you've done 1.7 million times before

What isn't TDD?

TDD IS NOT A MAGIC BULLET.

It will not make your code have less bugs...

... by definition, you are writing more code - therefore you might have more bugs.

... but it might help you catch them quicker.

... and it might help you ship fewer bugs.

What isn't TDD?

TDD IS NOT A MAGIC BULLET.

It will not provide rigorous proof that your code works...

... but it might alert you when new features break existing code.

... and may encourage you to write more maintainable code.

What isn't TDD?

TDD IS NOT A MAGIC BULLET.

It will not make your code easier to read or maintain...

... only you can do that.

... but it can give you hints, if you're listening for them.

... and it can be used as a secondary form of documentation.

Seven commandments of Testing

  1. Test no other code but your own
  2. Don't look for green until you've seen red
  3. Don't pee in the pool - set up responsibly
  4. Don't run your tests in production
  5. Don't push if your tests are failing
  6. Don't test what you don't care about
  7. Don't touch the database unless you absolutely must

How do I express an intent? (smalltalk)


Class: SetTestCase
  superclass: TestCase
  instance variables: empty full

SetTestCase>>setUp
  empty := Set new.
  full := Set
  with: #abc
  with: 5

SetTestCase>>testAdd
  empty add: 5.
  self should: [empty includes: 5]
					
(Kent Beck's original paper on Testing in Smalltalk)

How do I express an intent? (translated)


class SetTestCase < TestCase
  def setup
    @empty = []
    @full = ['abc', 5]
  end

  def test_add
    @empty << 5
    should(@empty.include? 5)
  end
end
					

How do I express an intent? (actual runnable ruby)


require 'minitest/autorun' # found in ruby 1.9's standard library

class ArrayTestCase < MiniTest::Unit::TestCase # A little more verbose
  def setup
    @empty = []
    @full = ['abc', 5]
  end

  def test_add
    @empty << 5
    assert(@empty.include? 5)
  end
end
					

How do I express an intent? (rails)


require 'test_helper'

class ArrayTestCase < ActiveSupport::TestCase # inherits from MiniTest::Unit::TestCase
  def setup
    @empty = []
    @full = ['abc', 5]
  end

  def test_add
    @empty << 5
    assert(@empty.include? 5)
  end
end
					

How do I express an intent? (rails + dsl)


require 'test_helper'

class ArrayTestCase < ActiveSupport::TestCase
  setup do
    @empty = []
    @full = ['abc', 5]
  end

  test 'should add items to empty arrays' do
    @empty << 5
    assert(@empty.include? 5)
  end
end
					

There are more! BDD it up!

MiniTest has a "spec" syntax too.

I would highly recommend trying it - it takes the readability further, but it does require a little more setup.

https://github.com/metaskills/minitest-spec-rails

Also, you might want to look at RSpec - we might do another presentation on this later

When things go right

By running
Run options: --seed 41230

# Running tests:

.

Finished tests in 0.092020s, 10.8672 tests/s, 10.8672 assertions/s.

1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
					

Welcome the TDD Police

  1. Test no other code but your own
  2. Don't look for green until you've seen red
  3. Don't pee in the pool - set up responsibly
  4. Don't run your tests in production
  5. Don't push if your tests are failing
  6. Don't test what you don't care about
  7. Don't touch the database unless you absolutely must

Let's get started

  1. Set up a rails project
  2. Add sconover/wrong to your gemfile
  3. Run the tests, fix any that are broken
  4. Write a new test
  5. Write the code that makes the test pass

Set up a rails project

What's wrong with no Wrong?

If I make a mistake in the above code, this is what I might get:

  1) Failure:
EventTest#test_should_add_the_number_5_to_the_array [/Users/matthew/projects/clients/nrug/nrug/test/models/event_test.rb:10]:
Failed assertion, no message given.

					

Wrong makes it all right

If I use wrong, I get this message instead:

  1) Error:
EventTest#test_should_add_the_number_5_to_the_array:
Wrong::Assert::AssertionFailedError: Expected @empty.include?(6), but
    @empty is [5]

    test/models/event_test.rb:10:in `block in <class:EventTest>'

					

How do I express an intent? (rails + wrong)


require 'test_helper'

class ArrayTestCase < ActiveSupport::TestCase
  setup do
    @empty = []
    @full = ['abc', 5]
  end

  test 'should add items to empty arrays' do
    @empty << 5
    assert { @empty.include? 6 }
  end
end
					

WARNING:

This will run your assert twice.

Add sconover/wrong

Open your Gemfile

Add the following line:

 gem "wrong" 

On the command line, run

 bundle install 

Lastly, in your test_helper.rb file, add the following lines


# At the end of your requires:
require 'wrong'

# Then, after `class ActiveSupport::TestCase`
include Wrong
					

Run the tests

From the commandline, to run all tests:

rake test

To run all tests in a particular file:

rake test test/models/event_test.rb

To run a particular test:

rake test test/models/event_test.rb test_that_something_happens

Alternatively, autotest! (autotest-rails, autotest-growl)

Fix the tests

Rails may have already provided you tests

... you might need to delete the fixture files. Don't worry, we won't be using them.

You are left with:

Write a test

Models are a good place to start:


require 'test_helper'

class EventTest < ActiveSupport::TestCase
  setup do
    @event = Event.new(
                        :title => "NRUG Episode II - Attack of the rubyists",
                        :description => "They're coming! " * 100,
                        :date => Date.today,
                        :time => Time.now,
                      )
  end

  test 'should present the date in a pretty manner' do
    @event.date = Date.parse "2013-10-15"
    assert { @event.pretty_date == "Tuesday 17 December 2013" }
  end
end
					

Run the test

You should see red:

# Running tests:

E

Finished tests in 0.116580s, 8.5778 tests/s, 0.0000 assertions/s.

  1) Error:
EventTest#test_should_present_the_date_in_a_pretty_manner:
NoMethodError: undefined method `pretty_date' for #<Event:0x007fc440684230>
    test/models/event_test.rb:15:in `block (2 levels) in <class:EventTest>'
    test/models/event_test.rb:15:in `block in <class:EventTest>'
					

Write the code to fix the test



class Event < ActiveRecord::Base

  # ... normal model gumpf ...

  def pretty_date
    date.strftime('%A %-d %B %Y')
  end

end
					

Run the test

You should STILL see red:

# Running tests:

E

Finished tests in 0.046519s, 21.4966 tests/s, 0.0000 assertions/s.

  1) Error:
EventTest#test_should_present_the_date_in_a_pretty_manner:
Wrong::Assert::AssertionFailedError: Expected (@event.pretty_date == "Tuesday 17 December 2013"), but
Strings differ at position 9:
 first: "Tuesday 15 October 2013"
second: "Tuesday 17 December 2013"
    @event.pretty_date is "Tuesday 15 October 2013"
    @event is #<Event id: nil, date: "2013-10-15", time: "2013-10-15 18:40:13", location: nil, title: "NRUG Episode II - Attack of the rubyists", event_description: nil&rt;

    test/models/event_test.rb:15:in `block in <class:EventTest&rt;'
					

Fix the test

Thankfully, wrong has told us what was wrong...


require 'test_helper'

class EventTest < ActiveSupport::TestCase
  setup do
    @event = Event.new(
                        :title => "NRUG Episode II - Attack of the rubyists",
                        :description => "They're coming! " * 100,
                        :date => Date.today,
                        :time => Time.now,
                      )
  end

  test 'should present the date in a pretty manner' do
    @event.date = Date.parse "2013-10-15"
    assert { @event.pretty_date == "Tuesday 15 October 2013" }
  end
end
					

Next step: ALWAYS REFACTOR.

Maintainability

Make your tests run fast

Track your coverage

Resources

All the links in this presentation, plus:

Any questions?

Code will be made available after the show