Test Doubles
What are Doubles
“Double” is a generic term for objects that look or behave like objects our code depends on. They are a fundamental part of unit testing.
The Types of Doubles
There are the following types / specifications of doubles: Dummies, Spies, Stubs, Fakes and Mocks.
Dummy
The purpose of a dummy is just to satisfy constructors. If you want to test a method which's object requires a dependency in the constructor that isn't used in the tested method, you pass in a dummy.
# order_processor.rb
class OrderProcessor
def initialize(logger)
@logger = logger
end
def greet()
"Hello!"
end
# ...
end
# order_processor_spec.rb
require 'rspec'
RSpec.describe OrderProcessor do
describe '#greet' do
it 'returns a greeting message' do
dummy_logger = double('Logger')
processor = OrderProcessor.new(dummy_logger)
expect(processor.greet()).to eq('Hello!')
end
end
end
Stub
A stub always returns the same value on a function call.
# example.rb
class Greeter
def initialize(name_source)
@name_source = name_source
end
def greet
"Hello, #{@name_source.name}!"
end
end
# example_spec.rb
require "rspec"
require_relative "tiny_example"
RSpec.describe Greeter do
it "greets using the fake name source" do
fake_name_source = double("NameSource", name: "Alice")
greeter = Greeter.new(fake_name_source)
expect(greeter.greet).to eq("Hello, Alice!")
end
end
Fake
When testing something that uses an external data source, that source can be replaced with a fake. Fakes can be used in unit and integration tests.
# example.rb
class Greeter
def initialize(database)
@database = database
end
def greet(user_id)
user_name = @database.find_user_name(user_id)
"Hello, #{user_name}!"
end
end
# fake_database.rb
class FakeDatabase
def initialize
@users = {
1 => "Alice",
2 => "Bob",
3 => "Charlie"
}
end
def find_user_name(user_id)
@users[user_id] || "Guest"
end
end
example_spec.rb
# example_spec.rb
require "rspec"
require_relative "example"
require_relative "fake_database"
RSpec.describe Greeter do
it "greets the user using a fake database" do
fake_db = FakeDatabase.new
greeter = Greeter.new(fake_db)
expect(greeter.greet(1)).to eq("Hello, Alice!")
expect(greeter.greet(2)).to eq("Hello, Bob!")
end
end
Spy
Spies are here to observe how the tested component interacts with another object. For example, if, how often or with what parameters a given method gets called. Spies can also be wrapped around real objects and just listen to the interactions with it.
# my_class.rb
class MyClass
def greet(user)
user.say_hello("Hello!")
end
end
# my_class_spec.rb
require "rspec"
RSpec.describe MyClass do
it "calls say_hello on the user" do
user = double("User").as_null_object
my_class = MyClass.new
my_class.greet(user)
expect(user).to have_received(:say_hello).with("Hello!")
end
end
There are also partial spies. A partial spy wraps around a real object and listens to a specific method call.
user = User.new
allow(user).to receive(:say_hello).and_call_original
spy_user = spy(user)
Mock
A mock is a double that contains predefined expectations.
# user_notifier_spec.rb
require 'rspec'
class UserNotifier
def initialize(mailer)
@mailer = mailer
end
def notify(user)
@mailer.send_email(user, "Welcome!")
end
end
RSpec.describe UserNotifier do
it "sends a welcome email" do
mailer_mock = double("Mailer")
allow(mailer_mock).to receive(:send_email)
notifier = UserNotifier.new(mailer_mock)
notifier.notify("alice@example.com")
expect(mailer_mock)
.to have_received(:send_email)
.with("alice@example.com", "Welcome!")
end
end
In other testing frameworks, the expectations are actually written inside the mock class.