Chad Woolley's blog



Chad WoolleyChad Woolley
Automated End-to-end Integration Testing for ActiveResource APIs
edit Posted by Chad Woolley on Thursday January 08, 2009 at 11:48PM

By popular demand, we're working on making the Pivotal Tracker API ActiveResource-compliant.

However, there are some quirks that are required to make ActiveResource happy. For example, when you are doing a 'create' or 'update' request, ActiveResource wants the response location to point to the 'show' URL for the new or updated record. For example, here's an ActiveResource 'create' call:

new_story = Story.create(
  :name => "New Story", 
  :requested_by => "Dan",
  :description => "Make API ActiveResource compliant")

On the controller, you must add the :location option to the render - you can't redirect:

render  :xml => xml, 
        :location => service_project_story_url(service_id, project_id, @story),
        :status => status

...otherwise, you get this helpful error from ActiveResource:

/Library/Ruby/Gems/1.8/gems/activeresource-2.2.2/lib/active_resource/base.rb:1006
        :in `id_from_response': undefined method `[]' for nil:NilClass (NoMethodError)
        from /Library/Ruby/Gems/1.8/gems/activeresource-2.2.2/lib/active_resource/base.rb:993:in `create'

This is the type of error which you will only catch through end-to-end testing with a real ActiveResource client hitting the running app. When I did the initial spike to see what problems we would run into, I wrote a simple manual script to run against the local development environment, hacking my way to a point which didn't blow up and I could visually inspect the output:

#!/usr/bin/env ruby
require 'rubygems'
require 'activeresource'
require 'pp'
class Story < ActiveResource::Base
  self.site = "http://localhost:3000/services/v1/projects/1"
  headers['TOKEN'] = '6cfc2055d1df5605241759014b06b232'
end

p "========================== Stories#create ====================================="
new_story = Story.create(:name => "New Story", :requested_by => "Dan", :description => "Make API ActiveResource compliant")
pp new_story

# etc for all other supported API actions...

However, now that we are doing the real non-spike implementation, we want to automate this end-to-end integration testing as part of our Continuous Integration. That way, we'll ensure that we are fully ActiveResource-compliant (against current and future versions), and that we don't have any inadvertent regressions due to future API bugfixes/enhancements.

Digging through the internets and rubyonrails-talk list archives turns up some discussion, but no good answers:

All of these mention using ActiveResource::HttpMock. However, as Eric and Xavier point out, there seem to be drawbacks to this approach. Plus, even if we get it to work, I'm worried the usage of HttpMock might mask some other issues related to authentication handling, or who knows what else. That's what real integration tests are for. Finally, HttpMock is an undocumented internal method that seems to exist in order to support Rails' test suite, so it's probably not a great idea to depend on that long term.

So, we don't have a great answer yet, but it seems clear that the highest-value, least-risk approach is to hit a real running app over HTTP with a real ActiveResource client.

The current plan is to leverage our existing Selenium RC test environment, which already has support for spinning up and managing a Rails server with the test environment. We can then port the manual spike tests above to automated ones which run as part of the selenium suite under Continuous Integration, and add appropriate assertions. This isn't optimal, though, because they won't actually use Selenium RC at all, which may confuse people. However, there's no sense reinventing the wheel (and adding time to the overall CI build) by spinning up a separate test server instance when we already spin one up for our selenium suite.

Let us know if you have any clever solutions.

-- Chad

Chad WoolleyChad Woolley
Notes on Google Chrome Compatiblity
edit Posted by Chad Woolley on Tuesday November 18, 2008 at 11:10PM

Pivot Jonathan and I were recently working on support for Google Chrome in Pivotal Tracker. Tracker's extensive JsUnit test suite made this a lot easier.

Here's some quick notes I took on the issues we ran into.

Don't try to directly mock the 'reset' method on a Form Element

This was the original mocking code in one of our JsUnit tests:

var resetCalled = false;
widget._uploadForm.reset = function() { resetCalled = true; };

This permanently blew away the "reset" method, so it was undefined when called in a subsequent. To fix it, we did this in our form builder method:

var element = Element.create("form");
element.nativeReset = element.reset;
element.reset = function() { element.nativeReset() };

Hash keys sort differently

We had a testHash.keys() being compared to a hardcoded array. Chrome sorted the keys differently (apparently non-deterministically, so we had to do an explicit sort:

assertArrayEquals(['10001', '10002', '10003', 'endOfList'].sort(), $H(itemListWidget.draggables).keys().sort());

It wasn't good to depend on the keys order in the first place, but it worked under IE, Firefox, and Safari.

The same hash sorting bug bit us in a much more obscure way. There was some threading test code that simulated timeouts/concurrency using a mock clock. Previously, the test code was dependent on the order in which the functions were added to a hash the mock "clock". This broke with a different hash sorting order. We had to simulate some additional "ticks" to make the test pass.

Mozilla, but not Gecko

The browser string returned for Chrome by one of our utility functions, BrowserDetect.browser(), is "Mozilla". However, for some of our simulated keypress events in tests, the "Gecko" version did not work.

Specifically, we had to use "KeyboardEvent" instead of "KeyEvents", and "initKeyboardEvent" instead of "initKeyEvent". See the table in this mozilla doc page.

Here's the code we used to handle both cases:

evt = document.createEvent('KeyboardEvent');
if (typeof(evt.initKeyboardEvent) != 'undefined') {
  evt.initKeyboardEvent(eventName, true, true, window, false, false, false, false, options.keyCode, options.keyCode);
} else {
  evt.initKeyEvent(eventName, true, true, window, false, false, false, false, options.keyCode, options.keyCode);
}

The UserAgent (request.user_agent) returns

Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.3.154.9 Safari/525.19

The 'sort' function does not preserve order of equivalent elements

The following page outputs 'ACBD' under Chrome:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        "http://www.w3.org/TR/html4/loose.dtd">
<html>
<body>
<a href="#" onclick="alert(doSort()); return false;">Sort</a>
<script type="text/javascript">
  function doSort() {
    var myArray = [
      {id: "A", sortVal: 0},
      {id: "B", sortVal: 1},
      {id: "C", sortVal: 1},
      {id: "D", sortVal: 2}
    ];
    var sorted = myArray.sort(function(a,b) {return a.sortVal - b.sortVal});
    return sorted[0].id + sorted[1].id + sorted[2].id + sorted[3].id;
  }
</script>
</body>
</html>