ActiveResource and Testing

Eric Mill

ActiveResource is a pretty amazing implementation of REST, made in Ruby, for Rails. By the Rails core. Invented by DHH. It’s got good credentials. Sadly, it did not make it into Rails 1.2, so there is tons of buzz about it, but very little in the way of actual resources and active discussion. The only thorough treatment of ActiveResource out there I can find is by a Rails core member, and it’s good. I’m not going to re-cover everything it says, so definitely check it out.

Taking ARes Out For a Test Drive

The idea is that you can do something like this:

class User < ActiveResource::Base
  self.site = "https://thoughtbot.com"
end

u = User.find 1
u.email = "new@email.com"
u.save

By declaring a model in your app as inheriting from ActiveResource::Base (as opposed to ActiveRecord::Base), you get to work with remote objects as if they were local. User.find is doing a GET request to /users/1.xml (at https://thoughtbot.com in this example), and u.save is doing a PUT request to /users/1. The PUT is actually a POST with a parameter _method=put, but its heart is in the right place. Of course, the site you’re getting objects remotely from has to support this, and the easiest way to do this is with the new map.resources helpers that did make it into Rails 1.2.

Now I have the opportunity to use ActiveResource at work, and I get to do it in the most educational way I can think of—replacing a developed local User model with a remote User model. Naturally, I have pre-existing tests written that I need to have pass in order for the refactor to be considered complete. The idea is that permissions for this app (and other, sister apps) will all be governed by a parent application, and each child app gets the permissions for the logged in user from the parent app. We decided the easiest way was by sharing the User model through ActiveResource.

I don’t need to change any user data, or search by anything but ID, so I really only need a show action implemented. And, because I’m not just replacing a User model, but also a Role model (User has_many :roles), I’m going to have to return additional data in the User object that acts like the Role model I used to have. I also have to omit certain fields in the remote User model that aren’t appropriate for the child app to know. Since it’s only one action, and it’s going to have to contort the data, I don’t want to do map.resources and override the remote User model’s to_xml function—it makes more sense to just make my own .rxml view and fake the resource. ActiveResource‘s to_xml format is very easy to fake, even including a has_many relationship.

At /tools/:tool_id/users/:id.xml:

xml.instruct!
xml.user do
  xml.email @user.email
  xml.tag! "first-name", @user.first_name
  xml.tag! "remote-id", @user.remote_id, :type => "integer"

  xml.roles do
    @user.roles_in(@tool).each do |role|
      xml << role.to_xml(:skip_instruct => true, :only => [:title, :context])
    end
  end
end

How to best test your ActiveResource models is an open question right now, as far as I can tell. There’s no documentation, or even blog posts, that I can find, but there is an http_mock file included with ActiveResource, that is used in ActiveResource in its own tests, to test itself. The setup in ActiveResource’s test file for Base looks (something) like this:

def setup
  @matz  = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person')
  @david = { :id => 2, :name => 'David' }.to_xml(:root => 'person')

  ActiveResource::HttpMock.respond_to do |mock|
    mock.get    "/people/1.xml",  {}, @matz
    mock.get    "/people/2.xml",  {}, @david
    mock.put    "/people/1.xml",  {}, nil, 204
    mock.delete "/people/1.xml",  {}, nil, 200
    mock.get    "/people/99.xml", {}, nil, 404
    mock.get    "/people.xml",    {}, "<people>#{@matz}#{@david}</people>"
  end
end

This approach is interesting, because instead of mocking out the behavior of the Person model, they’re creating a mock Internet for the Person model to talk to. And, instead of using YAML fixtures, they’re using XML fixtures. And, because ActiveResource’s XML format is so simple, they’re just making a hash with a root and calling to_xml, and that’s fine. That’s a lot to take in.

So here’s my method for integrating ActiveResources into my unit and functional tests, which for me only involved editing test_helper, and required no change to any functional or unit test files. I’m sure it’s not the best possible method, but it achieves my goal of not changing how I write unit and functional tests. Here’s an excerpt of my test_helper.rb:

def self.all_fixtures
  fixtures :other, :normal, :models
  remote_fixtures
end

def self.user(name)
  path = File.join(RAILS_ROOT, "test", "remote_fixtures", "users", "#{name.to_s}.xml")
  return nil unless File.exists?(path)
  File.read path
end

def self.remote_fixtures
  ActiveResource::HttpMock.respond_to do |mock|
    mock.get "/tools/1/users/2.xml", {}, user(:eric)
    # some ActiveResource requests append these empty parameters, in any order
    # and you can't seem to use regexps with HttpMock right now
    mock.get "/tools/1/users/2.xml?include=&conditions=", {}, user(:eric)
    mock.get "/tools/1/users/2.xml?conditions=&include=", {}, user(:eric)
    mock.get "/tools/1/users/3.xml", {}, user(:matt)
    mock.get "/tools/1/users/4.xml", {}, user(:paper)
    mock.get "/tools/1/users/0.xml", {}, nil, 404
    mock.get "/tools/1/users/.xml", {}, nil, 404
  end
end

def users(name)
  case name
  when :eric
    User.find(2)
  when :matt
    User.find(3)
  when :paper
    User.find(4)
  else
    nil
  end
end

This is assuming I have XML files in test/remote_fixtures/users, named eric.xml, matt.xml, and paper.xml, that are the mock responses I want ActiveResource to think it is getting.

The primary drawback here is that I’m hard-coding specific fixture info into test_helper. I could address some of this by doing part of the logic dynamically, by reading filenames in the test/remote_fixtures/users directory. The secondary drawback is that you need to make sure remote_fixtures is being called in every test file. Since I was already using an all_fixtures helper at the top of each file to load in my YAML fixtures, I just included a call to remote_fixtures inside that, and I didn’t have to add anything.

ActiveResource is not at all meant to be ready for release, so any issues I have with it should not be taken as complaints, just as information to be aware of if you go to use it. The main issue that makes this process difficult is that every possible route ActiveResource could request needs to be listed in HttpMock; there’s no support for regular expressions. Depending on your app, there could be requests made to /users/.xml, or /users/0.xml, and if there is no mock route specified, an error will be thrown and it will halt your tests. Sometimes requests are made with empty parameters, like ?include=&condition=. I’m not clear yet on when this happens, but it does. Of course, using HttpMock may not be at all the way the developers of ActiveResource ultimately intend us to test ActiveResource objects; perhaps we will actually mock out the objects or the model, instead of The Internet.

Overall, my transition to using ActiveResource has gone very smoothly. The implementation is a beautiful example of the kind of convenience that REST is supposed to bring us. Even if it’s not part of Rails 1.2, I think it’s ready to be used, in at least small applications, right now.