After many years working with RSpec I discovered a nice little feature and a small gotcha with instance doubles.
I’ve used instance_double
since before it was ported from rspec-fire.
My practice and all the current Relish RSpec examples of instance_double
use the following format:
It turns out the actual instance_double()
method takes a string representing the class or just the class constant:
And the important part here is the parameter:
doubled_class(String, Class)
No String is required here and class constants are auto-verifying. According to a long discussion
on rspec-mocks this behavior exists to avoid some
auto-loading of classes that aren’t needed so that tests can be a bit faster in some cases. For me this breaks
the expectation that I the mock is actually verifying that the class and methods actually exist. If I
wanted just a pure mock I could just use double
. And for Rails projects that make up a lot of the day to day
paid developer work everything auto-loads anyway. Using class constants is just simpler. If you’re on a legacy
project you can probably just add the config to verify the strings ahead of time anyway with the following:
This example code shows what happens when you make a typo in the string constant name and you don’t have the config to verify set:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
require 'spec_helper'
require_relative '../../lib/users/policy_enforcer'
require_relative '../../lib/users/public_policy'
RSpec.describe Users::PolicyEnforcer do
describe '#allowed?' do
let(:policy_enforcer) { Users::PolicyEnforcer.new(public_policy) }
context 'with correct string instance_double class constant' do
let(:public_policy) { instance_double('Users::PublicPolicy') }
before do
allow(public_policy).to receive(:allowed?)
end
it 'returns true' do
policy_enforcer.allowed?
expect(public_policy).to have_received(:allowed?)
end
end
context 'with typo string instance_double class constant' do
let(:public_policy) { instance_double('Use::PublicPolicy') }
before do
allow(public_policy).to receive(:allowed?)
end
it 'lies and returns true' do
policy_enforcer.allowed?
expect(public_policy).to have_received(:allowed?)
end
end
context 'with a proper class constant instance_double' do
let(:public_policy) { instance_double(Users::PublicPolicy) }
before do
allow(public_policy).to receive(:allowed?)
end
it 'returns true' do
policy_enforcer.allowed?
expect(public_policy).to have_received(:allowed?)
end
end
context 'with an typoed class constant instance_double' do
let(:public_policy) { instance_double(User:PublicPolicy) }
before do
allow(public_policy).to receive(:allowed?)
end
it 'fails because the constant is not defined' do
policy_enforcer.allowed?
expect(public_policy).to have_received(:allowed?)
end
end
end
end
The result is the spec with typo string instance double class constant
on line 22 lies to you. It behaves like a plain old double and allows you to accept methods on classes that don’t exist.