Javascript testing has come a long way from the days when it didn't exist. We've touched on the Scriptaculous unit testing framework in this space before, but the nature of a web application that's heavily AJAX-dependent is that you need to be absolutely sure the server and the client are in agreement, and for testing purposes this means that your framework tests and your Javascript tests have to be on the same page.
That page has a few requirements. It needs to be accessible from an HTML page that is accessed via the file:/// protocol -- you'll always need the option of running your tests in isolation, locally, rather than only through rake test:javascripts. It needs to be something that can be populated and maintained easily by humans. It needs to be lightweight. And it needs to be usable by both our Rails tests and our Javascripts tests. So, given the Rails paradigm of using YAML to maintain test fixtures, why not start there? Let's create an ajax_fixtures.yml file in test/javascripts and populate it with the AJAX requests and responses we want to test, as YAML data:
requests:
save:
value: 1
quantity: 3
id: 1
responses:
save:
result: Item successfully saved
An integration test called test/integration/ajax_test.rb is the way we roll when it comes to testing all AJAX interactions from the server perspective. In the setup method, load up the Ajax requests so they're available to every test:
def setup
@ajax = YAML.load(File.open("#{Rails.root}/test/javascript/ajax_fixtures.yml").read)
end
Now, to test the "save" AJAX request, you can access the @ajax['requests']['save'] and see if sending that request has the desired effect:
test("saving") do
post(item_path(items(:one), :format => 'json'), :item => @ajax['requests']['save'].to_json)
assert_equal(3, assigns(:item).quantity)
end
Well, that's straightforward enough and, so far as it goes, unnecessary. But let's think of that YAML file as a contract between the server and client components of the app. The Javascript tests have to prove that the right circumstances will generate the requests and handle to the responses, while the Rails tests must prove that they can interpret the requests and provide the expected responses. The Rails tests can easily handle our YAML file, but the Javascript tests might need a little help. First, we need to the YAML to HTML so that all browsers can load it. This is easily done with a rake task which you can drop into lib/tasks/somefile.rake. Choose which rake testing tasks should invoke the YAML task -- the whole test suite, obviously, and any that might be AJAX-related:
task :test => 'ajax:prepare'
namespace :test do
[:integration, :javascript].each{ |t| task t => 'ajax:prepare' }
end
namespace :ajax do
desc "Prepare AJAX test fixtures"
task :prepare do
json = YAML.load(File.open("#{Rails.root}/test/javascript/ajax_fixtures.yml")).to_json
File.open("#{Rails.root}/test/javascript/ajax_fixtures.html", 'w').puts(json)
end
end
This snippet will execute before you run rake or one of the specified tasks, and will write out the structured YAML to a big blob of JSON which is not very human-readable, but your Javascript test file can grab via a simple Ajax call:
<script type="text/javascript">
new Ajax.Request('ajax_fixtures.html', {
asynchronous: false,
method: 'get',
onSuccess: function(t) {
expectedAjax = t.responseText.evalJSON();
}
});
</script>
Also in our javascript test file, we're going to call on Dr Nic for some help mocking out the Ajax requests:
<script src="assets/drnic_js_test_helpers.js" type="text/javascript"></script>
...
testSave: function() { with(this) {
Test.Ajax.setupMock('/items/1/save.json', function(request, response) {
ajaxRequestCount++;
ajaxRequestJSON = request.options.parameters.evalJSON();
}.bind(this));
ajaxRequestCount = 0;
ajaxRequestJSON = '';
// invoke javascript 'save' method //
assertEqual(1, ajaxRequestCount); // the Ajax Request has been called
// compare the expectedAjax['requests']['save'] data (from YAML)
// with the ajaxRequestJSON data actually passed to the Ajax.Request method
}}
Remember to run Test.Ajax.clearMocks() when you're done --the teardown method is the best place for this. Now you've loaded the same information into your Javascript test and your Rails test and if the two agree, then you're really getting somewhere.
Special bonus section!
Comparing expectedAjax and ajaxRequestJSON can get a bit tricky if you have complicated nested objects. Here's a little object comparison method I wrote for this purpose, seen here as a standalone function. It requires Prototype:
function hashEqual(a, b) {
if ((!a) && (!b)) { return true;}
if (!a.keys) { a = $H(a); } if (!b.keys) { b = $H(b);}
if ( String(a.keys().sort()) != String(b.keys().sort())) { return false; }
var r = a.keys().map(function(k) {
var a_val = a.unset(k);
var b_val = b.unset(k);
if (typeof(a_val) == 'object') {
return (hashEqual(a_val, b_val));
} else if (a_val != b_val) {
return false;
} else { return true;}
}.bind(this)).uniq();
return(r.reject(function(e) { return(e); }).length == 0);
};
Comments