Double Trouble

35
DOUBLE TROUBLE Clarity on test doubles.

description

Clarity on Test Doubles in Ruby Mocks, Stubs, Fakes, Dummies, Spies and such. Presented at Boston.rb, Philly.rb and NYC.rb.

Transcript of Double Trouble

Page 1: Double Trouble

DOUBLE TROUBLEClarity on test doubles.

Page 2: Double Trouble

TESTING PHASES

exercise

verify

teardown

let(:sox) { Team.new “Boston” }let(:drays) { Team.new “Tampa” }before { sox.play(drays) }

it “should set standings” do expect(drays).to be_losingend

after { standings.delete_all }

setup

Page 3: Double Trouble

TESTING PHASES

setup

exercise

verify

teardown

→let(:sox) { Team.new “Boston” }let(:drays) { Team.new “Tampa” }before { sox.play(drays) }

it “should set standings” do expect(drays).to be_losingend

after { standings.delete_all }

Page 4: Double Trouble

TESTING PHASES

excercise

setup

verify

teardown

let(:sox) { Team.new “Boston” }let(:drays) { Team.new “Tampa” }before { sox.play(drays) }

it “should set standings” do expect(drays).to be_losingend

after { standings.delete_all }

Page 5: Double Trouble

TESTING PHASES

verify

setup

exercise

teardown→

let(:sox) { Team.new “Boston” }let(:drays) { Team.new “Tampa” }before { sox.play(drays) }

it “should set standings” do expect(drays).to be_losingend

after { standings.delete_all }

Page 6: Double Trouble

TESTING PHASES

teardown

setup

exercise

verify

let(:sox) { Team.new “Boston” }let(:drays) { Team.new “Tampa” }before { sox.play(drays) }

it “should set standings” do expect(drays).to be_losingend

after { standings.delete_all }

Page 7: Double Trouble

WHAT IS A TEST DOUBLE?

Page 8: Double Trouble

WHAT IS A TEST DOUBLE?

SUT

Page 9: Double Trouble

DOC

WHAT IS A TEST DOUBLE?

SUT

Page 10: Double Trouble

DOC

WHAT IS A TEST DOUBLE?

SUT

Indirect Output

Page 11: Double Trouble

DOC

WHAT IS A TEST DOUBLE?

SUT

Indirect Output

Indirect Input

Page 12: Double Trouble

DOCTestDouble

WHAT IS A TEST DOUBLE?

SUT

Indirect Output

Indirect Input

Page 13: Double Trouble

TEST DOUBLE PATTERNS

•Dummy Object

• Fake Object

•Mock Object

• Test Stub

• Test Spy

Page 14: Double Trouble

DUMMY OBJECT

A placeholder that is passed to the SUT and never used

let(:side_a) { 1 }let(:side_b) { 2 }let(:dummy) { Object.new }

subject { HighSchoolTrig.hypotenuse(a, b, dummy) }

it { should eq 2.236 }

Page 15: Double Trouble

FAKE OBJECT

An object which replaces the real DOC with an alternate implementation of the same functionalityclass FakePiCalcdef pi; 3.14159; end

end

let(:radius) { 2 }

before { MyGeo.pi_calculator = FakePiCalc }

subject { MyGeo.circumference(radius) }

it { should eq 13 }

Page 16: Double Trouble

MOCKS, STUBS & SPIES

An example:

class User < ActiveRecord::Base

before_create :enqueue_welcome_message

def enqueue_welcome_message queue = Application.config.email_queue raise(“Failed to queue”) unless queue.push(email, “Welcome”) end

end

Page 17: Double Trouble

NO DOUBLES

let(:email) { “[email protected]” }

subject { User.create(email: email) }

it { should be_persisted }its(:username) { should eq email }

Page 18: Double Trouble

MOCK OBJECTAn object which replaces the real DOC that can verify indirect output from the SUT with expectationslet(:mock_queue) { double() }let(:email) { “[email protected]” }

before do Application.config.email_queue = mock_queueexpect(mock_queue).to receive(:push).with(email, “Welcome”)

endsubject { User.create(email: email) }

it { should be_persisted }its(:username) { should eq email }

Page 19: Double Trouble

TEST STUBAn object which replaces the real DOC to control indirect input to the SUTlet(:stub_queue) { double(push: true) }let(:email) { “[email protected]” }

before do Application.config.email_queue = stub_queueend

subject { User.create(email: email) }

it { should be_persisted }its(:username) { should eq email }

Page 20: Double Trouble

TEST SPYA more capable Test Stub allowing verification of indirect output from the SUTlet(:spy_queue) { double(push: true) }let(:email) { “[email protected]” }

before do Application.config.email_queue = spy_queueend

subject { User.create(email: email) }

it { should be_persisted }its(:username) { should eq email }it “should enqueue welcome message” do expect(spy_queue).to have_received(:push).with(email, “Ohai”)end

Page 21: Double Trouble

DESIGNING FOR DOUBLES

•Dependency Lookup

•Dependency Injection

Page 22: Double Trouble

class Buddy def good_friend?; on_tap.craft?; end

def on_tapFridge.cold_one

endend

describe Buddy, “serving coors” do # TODO control indirect input to the SUT it “should not be a good friend” do expect(subject).not_to be_good_friend end

Page 23: Double Trouble

DEPENDENCY LOOKUPclass Buddy def good_friend?; on_tap.craft?; end

def on_tapFridge.cold_one

endend

describe Buddy, “serving coors” do let(:coors) { double(craft?: false) }

before { Fridge.stubs(:cold_one) { coors } } it “should not be a good friend” do expect(subject).not_to be_good_friend end

Page 24: Double Trouble

class Buddyattr_accessor :fridge def good_friend?; on_tap.craft?; end

def [email protected]_one

endend

describe Buddy, “serving coors” do let(:coors) { double(craft?: false) } let(:stub_fridge) { double(cold_one: coors) } before { subject.fridge = stub_fridge } it “should not be a good friend” do expect(subject).not_to be_good_friend end

DEPENDENCY INJECTION

Page 25: Double Trouble

RETROFITTING

• Test-Specific Subclasses

• Test Hooks

Page 26: Double Trouble

class Buddy

attr_reader :supermarket

def make_breakfast(request=“Steak & eggs”)ingredients = supermarket.find(request)prepare(ingredients)

endend

Page 27: Double Trouble

TEST-SPECIFIC SUBCLASSESclass Buddy

attr_reader :supermarket

def make_breakfast(request=“Steak & eggs”)ingredients = supermarket.find(request)prepare(ingredients)

endend

class TestBuddy < Buddyattr_writer :supermarket

end

Page 28: Double Trouble

TEST HOOKSclass Buddyif ENV != “TEST”attr_reader :supermarket

elseattr_accessor :supermarket

end

def make_breakfast(request=“Steak & eggs”)ingredients = supermarket.find(request)prepare(ingredients)

endend

Page 29: Double Trouble

ARE THEY FOR YOU?

Page 30: Double Trouble

“MOCKIST” TDD / BDD

• Uses mocks for all DOCs

• Likes the test writing process to inform design decisions

• Tests in strict isolation

Page 31: Double Trouble

CLASSIC TDD

• Uses test doubles only for awkward DOCs, favoring “real” objects

•Minimizes coupling between tests and implementation

• Tests small clusters of components, not isolated units

Page 32: Double Trouble

CLASSIC TDDERS CONSIDER USING A TEST DOUBLE IF:

• The behavior of the DOC cannot be changed/observed

• Use of the DOC could cause unwanted side-effects

• The DOC is too slow

• The DOC doesn’t exist yet

Page 33: Double Trouble

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 Hacker News while your build runs

Page 34: Double Trouble

MORE

xUnit Test Patterns: xunitpatterns.com

Mocks aren’t Stubs by Martin Fowler: martinfowler.com/articles/mocksArentStubs.html

A case against a case against mocking and stubbing by David Chelimsky: blog.davidchelimsky.net/2008/12/11/a-case-against-a-case-against-mocking-and-stubbing/

Timecop for testing time-dependent code: github.com/travisjeffery/timecop

RSpec: rspec.info

MiniTest: ruby-doc.org/stdlib

Mocha: github.com/freerange/mocha