Working With Rspec: Testing Input and Output with Mocks
Lately, I’ve been working on getting more familiar with TDD and Rspec by writing tests first for every method of my programs. Though I’ve heard some loud criticisms of adhering strictly to the test-first design pattern (as well as a rebuttal or two), I’m mostly interested in giving it a serious try right now so that I can learn all of the techniques and make a more informed judgment about when (not) to test down the line.
For the most part, I’ve been applying the standard pattern with which I’m most familiar: expect(some_method).to eq(some_value)
. Making sure my methods behave as I expect, and working through those expectations sequentially, has generally resulted in more elegant code with fewer unnecessary dependencies.
However, one of the things that I’ve found more tricky is testing input and output.
At first, I was tripped up by the simple fact that methods which send output to the console always return nil
. How am I supposed to test different instances of output to the console if all of them will eq(nil)
?
Next, I discovered the output
matcher. If I have a method like this:
Then I can test it like this:
This felt like a glorious solution. Writing tests for all of my output would be a breeze!
Unfortunately, this approach has some downsides too.
First of all, it’s a real pain for long strings. I end up with massive tests that are just double checking I’ve correctly copied the exact same text between the spec and lib files - getting very bogged down in the implementation details while verifying very little of my program’s behavior.
Second, this test is going to fail if I decide to make small, relatively inconsequential changes. Say I’ve written in a typo and want to fix it - now I have to modify my code and my tests. That doesn’t seem like an optimal way of doing things.
Facing this dilemma, a mentor advised that I separate out what I’m printing to the console from the operations that do the printing. Now I’m using a mock to test that input and output methods are properly being called, and then passing specific strings to those operations. This has made testing both easier and more useful. Here’s how it works:
I begin with an input/output class. This is the place where specific messages will get sent in order to display in the console. It looks like this:
Now I can run this class in my program by passing $stdin
and $stdout
as my arguments for reader and writer, and I can also run tests that don’t depend on those inputs by passing in other arguments.
At first, I thought I’d test this class by passing in StringIO objects. I had seen that approach mentioned in some other posts while I was reading on the topic, and it seemed like a good place to start. It helped me test that input and output were conforming to what I’d expect given whatever I passed in.
However, I was still unhappy with some elements of this approach. This may just be germane to errors I was making along the way, but I found that the operations I was performing to make tests pass were resulting in code that wouldn’t actually work as I expected, and I didn’t like that I had to set whatever input I was going to expect when I initialized new instances of the class.
So, I set out to make a mock. This would be an object that I could pass in for my reader and writer not to see that input and output were conforming to expectations, but rather just to see that the appropriate methods were in fact being called. When I initialize my MockIO, I set certain instance variables to false (e.g. `@print_was_called` or `@gets_was_called`), and I overwrite the appropriate methods in my mock to toggle those values to true [edit, see below] return unique identifiers [end edit] if they are indeed called. When my tests passed, I know that I’m in fact calling the methods I wanted to access, and I can pass in whatever values I like.
This approach has the additional benefit of letting me separate out all of the actual text I want to output to the console. I put that stuff a separate class where I have methods that return (rather than puts
or print
) strings. I can test those methods without using the output
matcher, simply expecting returns of those method calls to include("part of the string")
.
So, altogether, I have a working test suite where the tests are less brittle and the code operates as expected.
Note: I can tell that there’s a lot left to learn about using mocks in different situations. Some of the resources I’ve found helpful and provactive can be found here, here, here, and here.
[Check out my new post on rolling my own mocks to see more about the implementation details]