I'm writing unit tests using Minitest with Ruby on Rails.
Occasionally I need to mock multiple things at once.
For example, when testing an action that triggers notifications to users, I might want to mock two external servers (example: SmsClient
and EmailClient
) in one go.
I can do this as follows:
test 'my test case' do
SmsClient.stub :send, nil do
EmailClient.stub :send, nil do
get my_controller_url
end
end
end
This works, however I now need to mock one more class on the same call.
I'm concerned that my nesting of these stub
calls is going to get out of control.
Question: Is there a way I can setup a mock on these services without using the nesting?
Note: These examples are just a hypothetical.
I don't know if there are any Minitest helpers to simplify this, but here is some core Ruby code that could reduce nesting.
stubs = [
proc { |block| proc { SmsClient.stub(:send, nil, &block) } },
proc { |block| proc { EmailClient.stub(:send, nil, &block) } },
]
tests = proc do
# ...
end
stubs.reduce(:<<).call(tests).call
# results in
# (stubs[0] << stubs[1]).call(tests).call
#
# aka
# stubs[0].call(stubs[1].call(tests)).call
#
# aka
# SmsClient.stub(:send, nil) do
# EmailClient.stub(:send, nil) do
# # ...
# end
# end
To understand this answer you have to know that:
some_method :a, :b do
# block code
end
Can also be written as:
block = proc do
# block code
end
some_method(:a, :b, &block)
Each item in the subs
array is a proc
, that accepts a block, and returns a new proc that is passed to another element.
stubs.reduce(:<<)
will create a composed proc that passes any argument to the last element, the return value of that proc is passed the the element before it.
double = proc { |n| n + n }
pow2 = proc { |n| n * n }
operations = [pow2, double]
operations.reduce(:<<).call(2) # the pow2 of the double of 2
# pow2.call(double.call(2))
# a = 2 + 2 = 4
# b = a * a = 4 * 4 = 16
#=> b = 16
stubs.reduce(:<<).call(tests) # does the same
# stubs[0].call(stubs[1].call(tests))
# a = proc { EmailClient.stub(:send, nil, &tests) }
# b = proc { SmsClient.stub(:send, nil, &a) }
#=> b = proc { SmsClient.stub(:send, nil, &proc { EmailClient.stub(:send, nil, &tests) }) }
Note that b
is still a proc so we need to call .call
again on the final return value.
Here is an image to help you understand what is happening in the code above:
Note that you could invert the nesting by changing :<<
into :>>
.
You could define a helper to abstract away the common logic:
def stub_all(*stubs, &test_block)
stubs.map! do |subject, *stub_args|
proc { |block| proc { subject.stub(*stub_args, &block) } }
end
stubs.reduce(:<<).call(test_block).call
end
stub_all(
[SmsClient, :send, nil],
[EmailClient, :send, nil],
) do
# ...
end