Testing Code That Uses External Resources

Credit: John-Mason Shackelford

Problem

You want to test code without triggering its real-world side effects. For instance, you want to test a piece of code that makes an expensive network connection, or irreversibly modifies a file.

Solution

Sometimes you can set up an alternate data source to use for testing (Rails does this for the application database), but doing that makes your tests slower and imposes a setup burden on other developers. Instead, you can use Jim Weirichs FlexMock library, available as the flexmock gem.

Heres some code that performs a destructive operation on a live data source:

class VersionControlMaintenance DAY_SECONDS = 60 * 60 * 24 def initialize(vcs) @vcs = vcs end def purge_old_labels(age_in_days) @vcs.connect old_labels = @vcs.label_list.select do |label| label[date] <= Time.now - age_in_days * DAY_SECONDS end @vcs.label_delete(*old_labels.collect{|label| label[ ame]}) @vcs.disconnect end end

This code would be difficult to test by conventional means, with the vcs variable pointing to a live version control repository. But with FlexMock, its simple to define a mock vcs object that can impersonate a real one.

Heres a unit test for VersionControlMaintenance#purge_old_labels that uses Flex-Mock, instead of modifying a real version control repository. First, we set up some dummy labels:

require ubygems require flexmock require est/unit class VersionControlMaintenanceTest < Test::Unit::TestCase DAY_SECONDS = 60 * 60 * 24 LONG_AGO = Time.now - DAY_SECONDS * 3 RECENT = Time.now - DAY_SECONDS * 1 LABEL_LIST = [ { ame => L1, date => LONG_AGO }, { ame => L2, date => RECENT } ]

We use FlexMock to define an object that expects a certain series of method calls:

def test_purge FlexMock.use("vcs") do |vcs| vcs.should_receive(:connect).with_no_args.once.ordered vcs.should_receive(:label_list).with_no_args. and_return(LABEL_LIST).once.ordered vcs.should_receive(:label_delete). with(L1).once.ordered vcs.should_receive(:disconnect).with_no_args.once.ordered

Then we pass our mock object into the class we want to test, and call purge_old_labels normally:

v = VersionControlMaintenance.new(vcs) v.purge_old_labels(2) # The mock calls will be automatically varified as we exit the # @FlexMock.use@ block. end end end

Discussion

FlexMock lets you script the behavior of an object so that it acts like the object you don want to actually call. To set up a mock object, call FlexMock.use, passing in a textual label for the mock object, and a code block. Within the code block, call should_receive to tell the mock object to expect a call to a certain method.

You can then call with to specify the arguments the mock object should expect on that method call, and call and_returns to specify the return value. A call to #once indicates that the tested code should call the method only one time, and #ordered indicates that the tested code must call these mock methods in the order in which they are defined.

After the code block is executed, FlexMock verifies that the mock objects expectations were met. If they weren (the methods weren called in the right order, or they were called with the wrong arguments), it raises a TestFailedError as any Test::Unit assertion would.

The example above tells Ruby how we expect purge_old_labels to work. It should call the version control systems connect method, and then label_list. When this happens, the mock object returns some dummy labels. The code being tested is then expected to call label_delete with "L1" as the sole parameter.

This is the crucial point of this test. If purge_old_labels is broken, it might decide to pass both "L1" and "L2" into label_delete (even though "L2" is too recent a label to be deleted). Or it might decide not to call label_delete at all (even though "L1" is an old label that ought to be deleted). Either way, FlexMock will notice that purge_old_labels did not behave as expected, and the test will fail. This works without you having to write any explicit Test::Unit assertions.

FlexMock lives up to its name. Not only can you tell a mock object to expect a given method call is expected once and only once, you have a number of other options, summarized in Tables 17-1 and 17-2.

Table 17-1. From the RDoc

Specifier

Meaning

Modifiers allowed?

zero_or_more_times

Declares that the message may be sent zero or more times (default, equivalent to at_least.never)

No

once

Declares that the message is only sent once

Yes

twice

Declares that the message is only sent twice

Yes

never

Declares that the message is never sent

Yes

times(n)

Declares that the message is sent n times

Yes

Table 17-2. From the RDoc

Modifier

Meaning

at_least

Modifies the immediately following message count declarator to mean that the message must be sent at least that number of times; for instance, at_least.once means that the message is expected at least once but may be sent more than once

at_most

Similar to at_least, but puts an upper limit on the number of messages

Both the at_least and at_most modifiers may be specified on the same expectation.

Besides listing a mock methods expected parameters using with(arglist), you can also use with_any_args (the default) and with_no_args. With should_ignore_missing, you can indicate that its okay for the tested code to call methods that you didn explicitly define on the mock object. The mock object will respond to the undefnied method, and return nil.

Especially handy is FlexMocks support for specifying return values as a block. This allows us to simulate an exception, or complex behavior on repeated invocations.

# Simulate an exception in the mocked object. mock.should_receive(:connect).and_return{ raise ConnectionFailed.new } # Simulate a spotty connection: the first attempt fails # but when the exception handler retries, we connect. i = 0 mock.should_receive(:connect).twice. and_return{ i += 1; raise ConnectionFailed.new unless i > 1 } end

Test-driven development usually produces a design that makes it easy to substitute mock objects for external dependencies. But occasionally, circumstances call for special magic. In such cases Jim Weirichs class_intercepter.rb is a welcome ally.

The class below instantiates an object which connects to an external data source. We can touch this data source when we e testing the code.

class ChangeHistoryReport def date_range(label1, label2) vc = VersionControl.new vc.connect dates = [label1, label2].collect do |label| vc.fetch_label(label).files.sort_by{|f|f[date]}.last[date] end vc.disconnect return dates end end

How can we test this code? We could refactor itintroduce a factory or a dependency injection scheme. Then we could substitute in a mock object (although in this case, wed simply move the complex operations to another method). But if we are sure we "aren going to need it" (as the saying goes) and since we are programming in Ruby and not a less flexible language, we can test the code as is.

As before, we call FlexMock.use to define a mock object:

require class_intercepter require est/unit class ChangeHistoryReportTest < Test::Unit::TestCase def test_date_range FlexMock.use(vc) do |vc| # initialize the mock vc.should_receive(:connect).once.ordered vc.should_receive(:fetch_label).with(LABEL1).once.ordered vc.should_receive(:fetch_label).with(LABEL2).once.ordered vc.should_receive(:disconnect).once.ordered vc.should_receive(:new).and_return(vc)

Heres the twist: we reach into the ChangeHistoryReport class and tell it to use our mock class whenever it wants to use the VersionControl class:

ChangeHistoryReport.use_class(:VersionControl, vc) do

Now we can use a ChangeHistoryReport object without worrying that it will operate against any real version control repository. As before, the FlexMock framework takes care of making the actual assertions.

c = ChangeHistoryReport.new c.date_range(LABEL1, LABEL2) end end end end

See Also

Категории