Things are very different in the Ruby world. While using C#, Dependency Injection was something I used religiously; however, using Ruby I can simply stub the new method of a dependency. C# had hurdles that made stubbing controversial; however, using stubs in Ruby tests is basically seamless thanks to Mocha. Mocha makes it easy to Replace Collaborators with Stubs, but you shouldn't stop there--you should also Replace Mocks with Stubs.
Replacing Mocks with Stubs is beneficial for several reasons. Stubs are more concise. One of the largest problems with tests is that they can be overwhelming the first time you look at them. Reducing a 3 line mock definition to
stub(:one => 1, :two => 2)
is almost always a readability win.Stubs are great, but you may need to use a mock to verify behavior. However, you don't need to verify several behaviors in one test. Using a single mock to verify specific behavior and stubbing all other collaborations is an easy way to create tests that focus on essence. Following the One Expectation per Test suggestion often results in using one or more stubs... and more robust tests.
Using stubs is a good first step towards concise and relevant test code; however, you can take stubs a step further. Mocha defines a
stub_everything
method that creates an object which will return nil to any message it doesn't understand. The stub_everything stubs are fantastic for dependencies that are used several times, but you are only interested in specific interactions. The interactions which are unimportant for the current test no longer need to be defined.Stubs can be even more valuable if you are interested verifying a single interaction but are completely uninterested in all other interactions. In that case it's possible to define a
stub_everything
and set an expectation on it. Consider the following sample code.
message.from = current_user
message.timestamp
message.sent = Gateway.process(message)
end
Gateway.stubs(:process).returns(true)
message = stub_everything
message.expects(:sent=).with(true)
MessageService.deliver(message)
end
Since the
expects
method is defined on Object you can set expectations on concrete objects, mocks, and stubs. This results in a lot of possible combinations for defining concise behavior based tests.Lastly stubbing can help with test essence by allowing you to stub methods you don't care about. The following example defines a gateway that creates a message, sends the message, and parses the response.
response = post(create_request(message_text))
parse_response(response)
end
# ...
end
# ...
end
# ...
end
The Gateway#process method is the only method that would be used by clients. In fact, it probably makes sense to make post, create_request, and parse_response private. While it's possible to test private methods, I prefer to create tests that verify what I'm concerned with and stub what I don't care about. Using partial stubbing I can always test using the public interface.
gateway = MessageGateway.new
gateway.expects(:post).with("<text>hello world</text>")
gateway.stubs(:parse_response)
gateway.process("hello world")
end
gateway = MessageGateway.new
gateway.stubs(:create_request).returns("<text>hello world</text>")
gateway.expects(:parse_response).with("<status>success</status>")
gateway.process("")
end
gateway = MessageGateway.new
gateway.stubs(:create_request)
gateway.stubs(:post).returns("<status>success</status>")
assert_equal true, gateway.process("")
end
The combination of partial stubbing and defining small methods results in highly focused tests that can independently verify behavior and avoid cascading failures.
Mocha makes it as easy to define a mock as it is to define a stub, but that doesn't mean you should always prefer mocks. In fact, I generally prefer stubs and use mocks when necessary.
This is a newbie question so please excuse me if this sounds stupid.
ReplyDeleteHow can I create mocks/stubs of classes/collaborators that I do not have direct access to in the test class?
I have a class say ProcessController that has a public method say run(). This method in turn collaborates with various other classes to get some work done.
In my test method, I can only instantiate ProcessController and make a call to run() method. The collaborators that this method uses are all internal to its implementation. the dependencies are hard wired as this is legacy code written 6-7 years ago.
I now wish to stub out some of these collaborators in order to unit test the code. Is there an easy way to do so?
What would you suggest as the best way to unit test such code.
you should probably have a look at the not a mock framework - http://notahat.com/not_a_mock which tidies up a lot of that mocking and stubbing in rspec
ReplyDeleteThere are a few ways to handle the situation, which depend on what your code looks like.
ReplyDeleteYou can stub the method that creates collaborators (example).
You can also subclass ProcessController and redefine the methods that create the dependencies.
If the majority of your time is spent working with Legacy Code I'd highly suggest Working with Legacy Code by Michael Feathers. It's a good book.
Cheers, Jay
Hi Jay,
ReplyDeleteI've been reading you for a while and been concluding that one of us is clearly insane. It's nice to see that we're starting to converge:
http://www.madstop.com/ruby/jay_and_i_converge_on_testing.html
Now if you would only see the wisdom of applying DRY to your whole code base, rather than just the functional bit. :)
Jay,
ReplyDeleteWe're working with a very large ( 25k LOC ) Rails code base and we've been through multiple refactorings, especially with the help of flog.Largish methods refactored into multiple smaller and less complex ones.
Learnt the following by doing that :
The ability to easily stub behavior in tests is an indication of well refactored code.
Top down refactoring via stubs in tests to clean up and refactor existing code yields not only good API, but also less brittle tests.
Thoughts ?
Lourens Naude,
ReplyDeleteI think you are exactly right. That's one of my favorite aspects of designing tests in this way.
Cheers, jay