Slide 1

Slide 1 text

DOUBLE TROUBLE Clarity on test doubles.

Slide 2

Slide 2 text

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_losing end ! after { standings.delete_all } setup

Slide 3

Slide 3 text

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_losing end ! after { standings.delete_all }

Slide 4

Slide 4 text

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_losing end ! after { standings.delete_all }

Slide 5

Slide 5 text

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_losing end ! after { standings.delete_all }

Slide 6

Slide 6 text

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_losing end ! after { standings.delete_all }

Slide 7

Slide 7 text

DOC Test Double WHAT IS A TEST DOUBLE? SUT Indirect Output Indirect Input

Slide 8

Slide 8 text

TEST DOUBLE PATTERNS • Dummy Object • Fake Object • Mock Object • Test Stub • Test Spy

Slide 9

Slide 9 text

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 }

Slide 10

Slide 10 text

FAKE OBJECT An object which replaces the real DOC with an alternate implementation of the same functionality class FakePiCalc def pi; 3.14159; end end ! let(:radius) { 2 } ! before { MyGeo.pi_calculator = FakePiCalc } ! subject { MyGeo.circumference(radius) } ! it { should eq 13 }

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

NO DOUBLES let(:email) { “tom@crui.se” } ! subject { User.create(email: email) } ! it { should be_persisted } its(:username) { should eq email }

Slide 13

Slide 13 text

MOCK OBJECT An object which replaces the real DOC that can verify indirect output from the SUT with expectations let(:mock_queue) { double() } let(:email) { “tom@crui.se” } ! before do Application.config.email_queue = mock_queue expect(mock_queue).to receive(:push).with(email, “Welcome”) end subject { User.create(email: email) } ! it { should be_persisted } its(:username) { should eq email }

Slide 14

Slide 14 text

TEST STUB An object which replaces the real DOC to control indirect input to the SUT let(:stub_queue) { double(push: true) } let(:email) { “tom@crui.se” } ! before do Application.config.email_queue = stub_queue end ! subject { User.create(email: email) } ! it { should be_persisted } its(:username) { should eq email }

Slide 15

Slide 15 text

TEST SPY A more capable Test Stub allowing verification of indirect output from the SUT let(:spy_queue) { double(push: true) } let(:email) { “tom@crui.se” } ! before do Application.config.email_queue = spy_queue end ! 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

Slide 16

Slide 16 text

DESIGNING FOR DOUBLES • Dependency Lookup • Dependency Injection

Slide 17

Slide 17 text

class Buddy def good_friend?; on_tap.craft?; end ! def on_tap Fridge.cold_one end end ! 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

Slide 18

Slide 18 text

DEPENDENCY LOOKUP class Buddy def good_friend?; on_tap.craft?; end ! def on_tap Fridge.cold_one end end ! 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

Slide 19

Slide 19 text

class Buddy attr_accessor :fridge def good_friend?; on_tap.craft?; end ! def on_tap @fridge.cold_one end end ! 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

Slide 20

Slide 20 text

RETROFITTING • Test-Specific Subclasses • Test Hooks

Slide 21

Slide 21 text

class Buddy ! attr_reader :supermarket ! ! ! ! def make_breakfast(request=“Steak & eggs”) ingredients = supermarket.find(request) prepare(ingredients) end end ! ! !

Slide 22

Slide 22 text

TEST-SPECIFIC SUBCLASSES class Buddy ! attr_reader :supermarket ! ! ! ! def make_breakfast(request=“Steak & eggs”) ingredients = supermarket.find(request) prepare(ingredients) end end ! class TestBuddy < Buddy attr_writer :supermarket end

Slide 23

Slide 23 text

TEST HOOKS class Buddy if ENV != “TEST” attr_reader :supermarket else attr_accessor :supermarket end ! def make_breakfast(request=“Steak & eggs”) ingredients = supermarket.find(request) prepare(ingredients) end end ! ! !

Slide 24

Slide 24 text

ARE THEY FOR YOU?

Slide 25

Slide 25 text

“MOCKIST” TDD / BDD • Uses mocks for all DOCs • Likes the test writing process to inform design decisions • Tests in strict isolation

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

greg@promptworks.com