How do I temporarily replace methods with other methods in my unit tests?

977 views Asked by At

Using Mocha to mock or stub away a function that goes across the network is useful:

def test_something
  NetworkClass.stub(:fetch_something, "contrived example")

  assert_equal "contrived example", NetworkClass.fetch_something
end

But more often, my network-crossing functions take some input, and that input affects what is returned by the function. For my unit tests then, I just prepare a Hash with the inputs and outputs that my test suite needs to run. The problem is, now I can't use Mochas .stub or .expects anymore, because they don't allow me to substitute a function that actually does something.

Currently I'm doing something like this:

def setup
  @subject = NetworkClass.new

  test_data = fetch_test_data

  @subject.define_singleton_method(:fetch_something) { |input|
    test_data[input] or fail "Unexpected stub input: #{input}"
  }
end

But this permanently alters the subject, so I can't use this method to stub module functions or class methods.

Is there a way to "temporarily" replace a method with another method that is as simple as Mocha's .stub?

If somebody would like to tell me that this is a bad way to test and inform me of a superior one, I guess I'd also accept that answer. This has been bugging me for a while.


One obvious way to avoid this would be to stub the method in every test function with a specific input in mind, but that seems to me like a lot of extra "boilerplate" test code for no actual benefit. Also, this way, I can be sure that I don't forget to stub the method somewhere and accidentally go over the network every time I run my test suite.

2

There are 2 answers

0
Alexander On

Like you said, if you know what the input will be in the test you could probably still use mocha's functionality.

NetworkClass.stub(:fetch_something).with(input).returns(test_data[input])

This will be executed only if fetch_something is called with the correct input. You can make several stubs this way with the different inputs.

In general, though, this shouldn't be a problem as your tests should be predictable and repeatable. This means each separate test case should run the same code path and thus trigger the same arguments for your NetworkClass. Also each test case should run a single code path and be focused as much as possible.

Edit

Not sure what your testing framework is, but there should be mechanisms for setup and teardown before each testcase. This way you could use your approach and revert the class after each testcase.

0
Andrew Kozin On

In RSpec 3 you can use allow

allow(NetworkClass).to receive(:fetch_something).with(input) { test_data[input] }

Or (for more granulated testing)

allow(NetworkClass).to receive(:fetch_something) do |arg|
  expect(arg).to eql test_data[input]
end