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 ActiveResource
s 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.