Monday, September 03, 2007

Testing ActiveResource

I recently started using ActiveResource on a project. ActiveResource is the client side of the REST picture, that is it can consume the REST style web services you get with Rails, think of it as ActiveRecord for HTTP.

One of the difficult parts of using ActiveResource is testing. You don't want to run it over a real HTTP connection, for one it would significantly slow down your tests and secondly you don't know what horrible side effects it might have. So you need some way to fake a HTTP connection and HTTP requests and responses. Enter the HttpMock class.

HttpMock is a fairly nice class put together by the Rails folk that allows you to register a bunch of request and response pairs for a fake HTTP connection. When you call a method on an ActiveResource in a test, it calls the HttpMock class instead of trying to create a real connection. It is fairly easy to use, although there is no documentation the blogging community has come to the rescue.

Here is a quick example:

  @matz  = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person')
ActiveResource::HttpMock.respond_to do |mock|
mock.get "/people/1.xml", {}, @matz
end


This creates a request response pair so that when ActiveResource tries to GET /people/1.xml it gets back the xml for the @matz hash. Pretty simple.

However there is a problem. HttpMock is misnamed - HttpMock is a stub not a mock. But the difference is more than semantic, a mock object, like those provided by the excellent Mocha library, will allow you to set expectations on a object. An expectation is like saying "for this test to pass, this method with these arguments must be called on this object". HttpMock doesn't do that, instead it is just a stub that returns a value when a method is called with a certain argument, there is no verification that the method is actually called.

To show why this is bad, lets look at the test for resource deletion from the ActiveResource unit tests themselves.

Firstly, in the setup method we use HttpMock to create response for the delete method used on the "/people/1.xml" path:

  ActiveResource::HttpMock.respond_to do |mock|
mock.delete "/people/1.xml", {}, nil, 200
end


And here is the test_delete method:

    1   def test_delete
2 assert Person.delete(1)
3 ActiveResource::HttpMock.respond_to do |mock|
4 mock.get "/people/1.xml", {}, nil, 404
5 end
6 assert_raises(ActiveResource::ResourceNotFound) { Person.find(1) }
7 end


So what is happening here? Firstly on line 2 of the test_delete method we call the Person classes delete method. The delete method is a nice one-liner:

      def delete(id, options = {})
connection.delete(element_path(id, options))
end


We can assume here that the the content of the delete method is sending a delete request to "/people/#{id}.xml", that makes sense. The rest of the test_delete method creates a request response pair that returns a 404 error when "/people/1.xml" is requested, all that does is test that ActiveResource::Base.find correctly handles 404.

So what ensures that the delete method does the right thing, all the test does is ensure that it returns something other than nil or false. Lets change the method and see if we can break the test. We'll just return true in the delete method:

      def delete(id, options = {})
true
end


After this change I re-ran the tests and they all passed. This can't be good. If you can effectively remove the body of a method you are testing and the tests still pass you don't have a very good test. So how can we fix it?

Well if HttpMock was truely a mock we could ensure that the HTTP delete method is called on the "/people/1.xml" path. Fortunately HttpMock stores every request it recieves in a class variable. So we can check that the request was received with an assertion.

Here is a new test_delete:

  def test_delete
assert Person.delete(1)
assert ActiveResource::HttpMock.requests.include?(ActiveResource::Request.new(:delete, "/people/1.xml", nil, {}))
end


This one now fails when the body of the delete method is removed and passes when it is put back in, so this way we know that the tests is actually testing the functionality of the method. However it is a bit ugly isn't it? It would be nice if HttpMock allowed you to do this:

    ActiveResource::HttpMock.expects do |http|
http.delete "/people/1.xml", {}, nil, 200
end


But I'll leave that for another blog post.

1 comment:

Anonymous said...

I am about to do this in a project. Thanks for the article.