More Testing Thoughts - When Mocks Can’t Help

The other day I found myself needing to test a class (this seems to happen a lot), but I ran into a sticky situation. I wanted to test the validity of an instance method, easy enough, and then later wanted to test another instance method that relies on the first. The problem lies in the fact that the first relies on an external service that doesn’t provide stable data, making it difficult, if not impossible to test.

A little background on the code you’re about to see. It is quite common that I must consume XML from internal services, but I have no way of knowing how much data will be returned with each request (as these feeds rely on other services). Downloading tons of XML and iterating over it (and performing actions based on the data) is slow, cumbersome and can cause annoying situations if it crashes in the middle. To help with scalability (the buzzword of the year!) the XML feed listens for 2 query string parameters, start_event and limit. Knowing this I built a small iterator object to wrap a Hpricot instance and fetch XML with only 100 rows, and to repeat until it has reached the end.

Boring business logic and methods have been removed from the following sample.

class XmlIterator
  include Enumerable

  ...

  def each
    while data = get_xml(build_url)
      results = (data/@search_term)
      results.each{|x| yield x}

      @start_event = (data/'activations').first['end_event']

      break if results.length < @limit
    end
  end

private
  def build_url
    query_string = "start_event=#{@start_event}&limit=#{@limit}"

    return "#{@path}?#{query_string}"
  end

  ...
end

As you can see, I’ve built my own enumerable object, but the each method is really a proxy back to a Hpricot doc object (produced in the get_xml method I have conveniently removed). It will fetch 100 rows, process them, and then fetch 100 more, and continue until the result set is returning less than 100, meaning it was the last “page” of data.

Now for the testing. Validating build_url was pretty simple, set some instance variables, call the method and compare the returned string. But what about each? I don’t want to hit the service to test the each method, it would really be great if I could have it look at a local file in my mocks directory. All I really care to test here is that the proxying of the iteration works as expected. I struggled with how to do this for a few hours.

My initial reaction was to mock up the HTTP service using something like WebBrick, but that sounded like a lot of work, and a maintenance nightmare in the future. I knew the solution was to change the build_url method, but how can I do that without overriding it completely, as I need to test the original implementation. This meant placing a file in test/mocks/test was out.

Then it hit me, why not just change the build_url for that one test. There are 2 ways to go about this (that I know of). Both can be seen in the example below:

require File.dirname(__FILE__) + '/../test_helper'

class XmlIteratorTest < ActiveSupport::TestCase
  def setup
    @iterator = XmlIterator.new("http://www.madeup.com")
  end

  def test_each
    def @iterator.build_url
      "#{RAILS_ROOT}/test/mocks/test/activations.xml"
    end

    ...
  end

  def test_each_again
    @iterator.instance_eval do
      def build_url
        "#{RAILS_ROOT}/test/mocks/test/activations.xml"
      end
    end

    ...
  end
end

Both @iterator.instance_eval and def @iterator.build_url will produce the same result, a new version of build_url for that specific instance of @iterator, it’s just a matter of preference which you chose to do. Personally, since it’s just one method I’m redefining I like the first version, but if I was overriding 2 or more I’d probably go with the second.

Mocking Net::HTTP For Testing

Quite often at work I find myself having to write code (and therefore tests) that use the Net::HTTP class to read XML from some external REST based service. In this brief article I will explain two minor updates I perform on the Net:HTTP and File classes when in my testing environment.

Most of the services I interact with provide QA and Staging environments along with their Production system. Often times though I am performing other actions with other external services based on the data I am reading from the XML document. Because of this I prefer to mock up the XML response with data that will allow me to test all cases.

The following code uses a few Rails only extensions like alias_method_chain. It is quite simple to factor these out, but I will leave that as a challenge to the reader.

module Net
   class HTTP
     def get_with_file(path, *args)
       case File.basename(path)
       when /activations/
         mock_path = File.join(File.dirname(__FILE__),  "activations.xml")
         return File.open(mock_path)
       else
         get_without_file(path, *args)
       end
     end
     alias_method_chain :get, :file
   end
end

The above class opens Net::HTTP and adds the get_with_file method. Its processing is fairly simple, it looks at the basename of the path provided and uses that to match inside a case statement. Using this case statement I can now open a file, or proceed to another method (get_without_file in this case). Following this method definition is a call to alias_method_chain that renames Net::HTTP.get to Net::HTTP::get_without_file, and links Net::HTTP.get to Net::HTTP::get_with_file.

When using a file to represent an HTTP response object you would normally use the following code and be done.

class File
  alias_method :body, :read
end

This will provide the desired results and make the File appear to be a simple HTTP response. The problem with the simple rewrite to File is that if you call response.body more than once, you will receive nil after the first try. This is because you have reached the end of the file the first time you read through it. To combat this problem I use the following code instead of the simple code above.

class File
   def read_with_memory
     @file_contents ||= read_without_memory
     return @file_contents
   end
   alias_method_chain :read, :memory
   alias_method :body, :read_with_memory

   def header
     {
       'status' => "201 Created",
       'location' => "http://somethingunimportant.com/orders/1"
     }
   end

   def code
     "201"
   end
end

The header and code methods aren’t at all required, but were needed due to the calling code of mine using them to verify the Net::HTTP response object before starting to process the data. The read_with_memory method, like the other two is the important change. This method reads the file into an instance variable and saves it. Each subsequent call returns the instance variable, and not the end of the File.

Rambling one post at a time