Yet another rehash of the same old TDD/BDD presentation.
This one was for the BT Adastral Park Software Craftsmanship Mini-Conference on 8 Sepctember 2009.
19. Automated Testing
class Adder
def add a, b
a + b
end
end
class AdderTest < Test::Unit::TestCase
def test_add
adder = Adder.new
assert_equal 4, adder.add(2, 2)
assert_equal 2, adder.add(4, -2)
end
end
20. Are You Really Testing
Your Code?
class Adder
def add a, b
a + b
end
end
class AdderTest < Test::Unit::TestCase
def test_add
assert_equal 4, 2 + 2
end
end
34. Three Rules
1. Do not write any production code unless it
is to make a failing unit test pass.
http://tinyurl.com/threerules
35. Three Rules
1. Do not write any production code unless it
is to make a failing unit test pass.
2. Do not write any more of a unit test than
is sufficient to fail; and compilation failures
are failures.
http://tinyurl.com/threerules
36. Three Rules
1. Do not write any production code unless it
is to make a failing unit test pass.
2. Do not write any more of a unit test than
is sufficient to fail; and compilation failures
are failures.
3. Do not write any more production code
than is sufficient to pass the one failing
unit test.
http://tinyurl.com/threerules
37. State-Based
class DongleTest < Test::Unit::TestCase
def test_wibble
# Set up test inputs
dongle = Dongle.new
dongle.addString("foo")
dongle.addRemoteResource("http://foo.com/bar")
# Exercise functionality under test
dongle.wibble!
# Verify results are as expected
assert_equal(42, dongle.answer)
end
end
40. More Descriptive Test
Names
class AdderTest < Test::Unit::TestCase
def test_should_add_two_positive_numbers
assert_equal 4, Adder.new.add(2, 2)
end
def test_should_add_a_positive_and_a_negative_number
assert_equal 2, Adder.new.add(4, -2)
end
end
41. RSpec
describe 'An adder' do
it 'can add two positive numbers' do
Adder.new.add(2, 2).should == 4
end
it 'can add a positive and a negative number' do
Adder.new.add(4, -2).should == 2
end
end
42. Generated
Documentation
$ spec -f s adder_spec.rb
An adder
- can add two positive numbers
- can add a positive and a negative number
Finished in 0.005493 seconds
2 examples, 0 failures
47. Describing Features
Feature: Transferring money between two accounts
Scenario: Simple transfer
Given an account called 'source' containing £100
And an account called 'destination' containing £50
When I transfer £20 from source to destination
Then the 'source' account should contain £80
And the 'destination' account should contain £70
48. Step Implementations
Given /^an account called '(w*)' containing £(d*)$/ do |name, amount|
@accounts ||= {}
@accounts[name] = Account.new(amount.to_i)
end
When /^I transfer £(d*) from (w*) to (w*)$/ do |amount, from, to|
AccountController.new.transfer @accounts[from],
@accounts[to],
amount.to_i
end
Then /^the '(w*)' account should contain £(d*)$/ do |name, amount|
@accounts[name].balance.should == amount.to_i
end
60. RSpec
describe 'Making a transfer' do
it 'debits the source account' do
controller = AccountController.new
from = mock 'from'
to = mock 'to'
to.stub :credit
from.should_receive(:debit).with 42
controller.transfer from, to, 42
end
end
63. Not-a-Mock
describe 'Making a transfer' do
it 'debits the source account' do
controller = AccountController.new
from = Account.new
to = Account.new
from.stub_method :debit => nil
to.stub_method :credit => nil
controller.transfer from, to, 42
from.should_have_received(:debit).with(42)
end
end
65. Mix stubs and mocks
describe 'Peter Petrelli' do
before do
@peter = PeterPetrelli.instance
@claire = mock Cheerleader
@world = mock World
@claire.stub :save
@world.stub :save
end
it 'saves the cheerleader' do
@claire.should_receive :save
@peter.fulfil_destiny
end
it 'saves the world' do
@world.should_receive :save
@peter.fulfil_destiny
end
end
76. Further Reading
Introducing BDD (Dan North)
http://dannorth.net/introducing-bdd
BDD Introduction
http://behaviour-driven.org/Introduction
Mock Roles, Not Objects
(Freeman, Mackinnon, Pryce, Walnes)
http://www.jmock.org/oopsla2004.pdf
BDD in Ruby (Dave Astels)
http://blog.daveastels.com/files/BDD_Intro.pdf
Notas del editor
Short presentation on the background and principles of TDD and BDD.
Think about what the code you&#x2019;re writing is supposed to do. Reduces temptation to write speculative code. Executable docs. Forces clean structure (or at least makes poor structure painful).
Think about what the code you&#x2019;re writing is supposed to do. Think about interfaces and responsibilities. Think about alternate paths (errors etc).
Reduces temptation to write speculative code.
Executable docs. (Almost) guaranteed up-to-date.
Forces clean structure (or at least makes poor structure painful).
Catches some regression bugs.
Makes refactoring safer (meaning you&#x2019;re more likely to do it).
Tests help catch regression problems, enable rapid deployment and give you the confidence to refactor. But that&#x2019;s just a benefit of unit testing, not TDD.
Not just the evolution of the practice, but steps most people go through in their understanding of TDD.
We&#x2019;ve all written code this way, and probably revert to it from time to time!
Manual testing (either by the developer or someone else).
Works at a small scale and in the short term, but unmaintainable.
Write test cases to prove that the code works.
Tests provide confidence that we haven&#x2019;t broken anything when making changes.
Classic (but not recommended) approach is to create one test case per production class, and one test method per production method.
This passes, but doesn&#x2019;t actually call our method. Obviously it&#x2019;s an extreme example, but it illustrates the problem of writing test cases when the code&#x2019;s already there.
How do we know the tests are correct?
Write them first, watch them fail, then write the code and check they pass.
This is an improvement, but means you have to have a fairly complete design before you start (or you are restricted to system-level integration tests, rather than unit testing each class/method.
This is an improvement, but means you have to have a fairly complete design before you start (or you are restricted to system-level integration tests, rather than unit testing each class/method.
This is an improvement, but means you have to have a fairly complete design before you start (or you are restricted to system-level integration tests, rather than unit testing each class/method.
Subtle difference from test-first: it&#x2019;s about design more than testing.
Make small changes, only writing enough to pass a test.
After each test, refactor code to a clean design.
Cycle should repeat at least every few minutes. Sometimes several times per minute.
Cycle should repeat at least every few minutes. Sometimes several times per minute.
Cycle should repeat at least every few minutes. Sometimes several times per minute.
Cycle should repeat at least every few minutes. Sometimes several times per minute.
Cycle should repeat at least every few minutes. Sometimes several times per minute.
To a large extent, BDD is &#x2018;TDD done well&#x2019;.
We&#x2019;re now specifying what the object under test should do, rather than just writing an uninformatively-named method to test it.
This is RSpec, a framework specifically designed for BDD.
Even if you&#x2019;re only using an xUnit framework, you can extract similar information if you give your test methods descriptive names.
In case the Java people were feeling left-out.
Rather than guessing what low-level objects we need and building the system up from there, we start with the actual requirement at the outside of the system (user inter, and discover the need for lower-level objects as we go.
Integration testing uses a state-based approach. Used for acceptance tests for new features, which then live on as regression tests.
Cucumber. Now also available for Java (Cuke4Duke), but see also JBehave and EasyB.
Test classes in isolation.
When we ask the object to perform a specific action, it should interact in a particular way with its collaborators. But how do we make that test pass if the collaborator classes haven&#x2019;t been written yet?
Mocks and stubs.
Stubs are used to mimic the behaviour of other parts of the system that you don&#x2019;t want to use for real. Mocks are used to specify interactions. Don&#x2019;t use mocks for library calls etc which you can&#x2019;t control &#x2013; create a thin wrapper instead, and mock calls to that (let the mocks drive the design of the wrapper API).
Boundary objects can be tested using a state-based approach, as mocks are obviously not applicable here.
Naive example! Also you wouldn&#x2019;t really have this code when you wrote the tests/specs.
Record/playback. Advantage: IDE/refactoring support. Disadvantage: doesn&#x2019;t necessarily encourage writing the test first.
Specify expectations in advance, then verify afterwards. Advantage: more declarative. Disadvantage: Weird syntax.
Specify expectations in advance. Verified automatically.
Also known as test spies.
Calling methods on mocks is silent. Verify that expected methods were called afterwards.
Alternative mocking framework for RSpec. Stub methods out first (on real instances), then verify that expected methods were called afterwards.
Testing behaviour means calling public methods and observing what the class under test does. You should not need to use tricks to test private methods or inspect instance variables.
Tests should be runnable individually and in any order. If one test depends on data set up by another, this will make it hard to maintain your test suite, and cause puzzling failures. This is what setup and teardown methods are for.
If each test only asserts one aspect of behaviour, it makes it obvious what&#x2019;s wrong when it fails. It also prevents the first failure from masking later assertions.
Use stubs and mocks to decouple the class under test from the rest of the system.
If the method under test is relying on a collaborator providing data, that can simply be stubbed &#x2013;&#xA0;there&#x2019;s no need to assert that the query happened. Use mocks to test that the code under test is passing the right messages to its collaborators.
Beware of going overboard with stubs and mocks. There&#x2019;s a happy medium between not using them at all and over-using them.
Don&#x2019;t use stubs and mocks in integration tests (other than possibly for interaction with other systems).
Listen to the tests. If they&#x2019;re hard to write or brittle, it&#x2019;s probably a sign that the design of the code could be improved.