WHAT IS A TEST DOUBLE?
Indirect Output
SUT DOC
Indirect Input
WHAT IS A TEST DOUBLE?
Indirect Output
SUT Test
DOC
Indirect Input Double
TEST DOUBLE PATTERNS
• Dummy Object
• Fake Object
• Test Stub
• Test Spy
• Mock Object
DUMMY OBJECT
A placeholder that is passed to the SUT and never used
def test_with_dummy
a = 1
b = 1
dummy = Object.new
hypotenuse = MyTrig.pythagorean(a, b, dummy)
assert_equal 1.414213562373095, hypotenuse
end
FAKE OBJECT
An object which replaces the real DOC with an alternate
implementation of the same functionality
def test_with_fake
MyGeo.pi_calculator = FakePie
radius = 2
assert_equal 13, MyGeo.circumference(radius).round
end
class FakePie
def pi; 3.14159; end
end
TEST STUB
An object which replaces the real DOC to control indirect
input
def test_with_stub
Time.now # => Tues Aug 11 08:13:05 -0400 2009
millenium = Time.parse(“2000/1/1 00:00:00 UTC”)
Time.stubs(:now).returns(millenium)
@redneck = Redneck.create(:last_name => “Federline”)
assert_equal millenium, @redneck.created_at
end
MOCK OBJECT
An object which replaces the real DOC that can verify indirect
output with expectations
def test_with_mock
mock_logger = mock.expects(:warn).with(“Out of range exception”)
CrazyRadar.logger = mock_logger
CrazyRadar.scan(“Tom Cruise”)
end
TEST SPY
A more capable Test Stub allowing verification of indirect output
def test_with_spy
spy_logger = stub(:warn)
CrazyRadar.logger = spy_logger
CrazyRadar.scan(“Tom Cruise”)
assert_received(spy_logger, :warn) {|expect|
expect.with(“Out of range exception”)
}
end
CONSIDER USING A TEST
DOUBLE IF:
• The DOC doesn't exist yet
• The behavior of the DOC cannot be changed/observed
• The DOC is too slow
• Use of the DOC could cause unwanted side-effects
• Visibility into SUT implementation details are required
OVERUSE CAN LEAD TO:
• Over specified tests of the SUT's process, not its result
• Fragile tests that break when implementation changes
• Untested integration
• Less time on Twitter while your build runs
DESIGNING FOR DOUBLES
• Dependency Injection
• Dependency Lookup
DEPENDENCY INJECTION
class Wife
cattr_accessor :fridge, :beverage
def self.beer_me(request=‘Guinness’)
beverage = @fridge.find_beer.open
end
end
def test_beer_quality
natty = stub(:quality).returns(‘poor’)
cooler = expects(:find_beer).returns(natty)
Wife.fridge = cooler
Wife.beer_me
assert_not_equal ‘top notch’, Wife.beverage.quality
end
DEPENDENCY LOOKUP
class Wife
cattr_accessor :beverage
def self.beer_me(request=‘Guinness’)
beverage = BeerFactory.create(request).open
end
end
def test_beer_quality
natty = stub(:quality).returns(‘poor’)
BeerFactory.expects(:create).returns(natty)
Wife.beer_me
assert_not_equal ‘top notch’, Wife.beverage.quality
end
RETROFITTING
• Test-Specific Subclasses
• Test Hooks
TEST-SPECIFIC SUBCLASSES
class Wife
cattr :supermarket
def self.make_breakfast(request=‘Steak & eggs’)
ingredients = supermarket.find(request)
prepare(ingredients)
end
end
class TestWife < Wife
cattr_accessor :supermarket
end
TEST HOOKS
class Wife
if ENV != ‘TEST’
cattr :supermarket
else
cattr_accessor :supermarket
end
def self.make_breakfast(request=‘Steak & eggs’)
ingredients = supermarket.find(request)
prepare(ingredients)
end
end
MORE
xUnit Test Patterns: http://xunitpatterns.com Mocha: http://github.com/floehopper/mocha
Ruby Mocking and Stubbing with rr by Josh RR: http://github.com/btakita/rr
Nichols:
http://technicalpickles.com/posts/ruby-stubbing- RSpec: http://github.com/dchelimsky/rspec
and-mocking-with-rr
Mocha fork with Test Spies by Joe Ferris:
Timecop for testing time-dependent code: http://github.com/jferris/mocha
http://github.com/jtrupiano/timecop
http://robots.thoughtbot.com/post/159805295/
GoFish: spy-vs-spy
http://github.com/gsterndale/GoFish
0 comments
Post a comment