14 June 2008

Ruby - Making a Mockery of Testing

Having gotten rcov working well, I now had to eat the bitter fruit of my low coverage. The main code talks to a news server, so testing would not be so simple as calling a few methods and checking the results. Perhaps I still need to refactor into smaller, more easily testable methods. I'll visit that at another time.

The solution is to "mock" the news server. A mock is code that provides the same interface (or at least enough of it for the tests) as a "real" object or class, yet return expected data each time the test is run. Mocks also can be configured to know how many times a method should be called, in what order they are called, the values that should be passed in and so on. If these conditions are not met, an error is thrown and the test fails.

Ruby has several tools for creating mock objects. I settled on Flex Mock after reading a few blogs. Others were passionate about Mocha - I'll take a look at that another time.

For the purposes of my test, a Ruby newsreaderAPI needs to provide the following methods:

  • Net::NNTP.new(host, port, timeout) - Specify the host, port and timeout values to use when connecting (class method)
  • Net::NNTP.connect() - Connect to the host with parameters set in new.
  • Net::NNTP.xover(groupname, :from=>low, :to=>high) - Retrieve the headers from group groupname in the range low..high
  • Net::NNTP.group(group) - Set the group to fetch articles from
  • Net::NNTP.article(id) - Retrieve the article with the given id

Notice there is one class method to mock. After a little looking, I found the answer in the README file (see section "Mocking Class Objects) for FlexMock. The answer is to have the class method return another flexmock object which is the instance.

nntp_mock = flexmock
... more stuff here to define nntp_mock...
flexmock(Net::NNTP).should_receive(:new).and_return(nntp_mock)

Now when the Net::NNTP.new method is called, it will return our flexmock instance which handles the rest of the test.

Lets look at the flexmock instance object now:

nntp_mock = flexmock
nntp_mock.should_receive(:connect).once.
with_no_args
nntp_mock.should_receive(:group).once.
with(String).and_return(group_mock)
nntp_mock.should_receive(:xover).once.
with(String, Hash).and_return([summary1, summary2])
nntp_mock.should_receive(:article).twice.
with(String).and_return(article1, article2)

One interesting feature is that parameters may be chained together. So for example the last line specified that:

  • It responds to the method call 'article'
  • It should be called exactly twice during the test (fail otherwise)
  • It must receive exactly one parameter which is a string
  • It returns the specified articles

So with just a few lines of code we've written a newsgroup reader application which is sufficient for our test. Its behavior is deterministic and further our test will fail if any of the expectations set for it fail. That is a lot of value for a small effort.

The README file is extensive and gave me enough information to write my mock.

The next time you need to write a test for code which references an external resource, mock it instead. You'll be happy you did.

No comments: