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

  • The FlexMock generated RDoc (http://onestepback.org/software/flexmock/)
  • class_intercepter.rb (http://onestepback.org/articles/depinj/ci/class_intercepter_rb.html)
  • Alternatives to FlexMock include RSpec (http://rspec.rubyforge.org/) and Test:: Unit::Mock (http://www.deveiate.org/projects/Test-Unit-Mock/)
  • Jim Weirichs presentation on Dependency Injection is closely related to testing with mock objects (http://onestepback.org/articles/depinj/)
  • Kent Becks classic Test Driven Development: By Example (Addison-Wesley) is a must read; even the seasoned TD developer will benefit from Kents helpful patterns section at the back of the book


Strings

Numbers

Date and Time

Arrays

Hashes

Files and Directories

Code Blocks and Iteration

Objects and Classes8

Modules and Namespaces

Reflection and Metaprogramming

XML and HTML

Graphics and Other File Formats

Databases and Persistence

Internet Services

Web Development Ruby on Rails

Web Services and Distributed Programming

Testing, Debugging, Optimizing, and Documenting

Packaging and Distributing Software

Automating Tasks with Rake

Multitasking and Multithreading

User Interface

Extending Ruby with Other Languages

System Administration



Ruby Cookbook
Ruby Cookbook (Cookbooks (OReilly))
ISBN: 0596523696
EAN: 2147483647
Year: N/A
Pages: 399

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net