Testing Code That Uses External Resources

Credit: John-Mason Shackelford


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.


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

	 def purge_old_labels(age_in_days)
	 old_labels = @vcs.label_list.select do |label|
	 label[date] <= Time.now - age_in_days * DAY_SECONDS
	 @vcs.label_delete(*old_labels.collect{|label| label[

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 	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
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|



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

	 v = VersionControlMaintenance.new(vcs)

	 # The mock calls will be automatically varified as we exit the
	 # @FlexMock.use@ block.


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



Modifiers allowed?


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



Declares that the message is only sent once



Declares that the message is only sent twice



Declares that the message is never sent



Declares that the message is sent n times


Table 17-2. From the RDoc




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


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
	 and_return{ i += 1; raise ConnectionFailed.new unless i > 1 }

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
	 dates = [label1, label2].collect do |label|
	 return dates

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

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)

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



Date and Time



Files and Directories

Code Blocks and Iteration

Objects and Classes8

Modules and Namespaces

Reflection and Metaprogramming


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

Similar book on Amazon

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