Ruby on Rails Minitest multiple mocks on one test

50 views Asked by At

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.

1

There are 1 answers

0
3limin4t0r On

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:

explanation code

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