Let's Make a Framework: JSON and Ajax Tests
Welcome to part 49 of Let’s Make a Framework, the ongoing series about building a JavaScript framework.
If you haven’t been following along, these articles are tagged with lmaf. The project we’re creating is called Turing.
JSON Content-Type Support
In this part I’m going to show you how to use server-side JavaScript to write some Ajax tests for Turing’s net module.
The net module should support form encoded data and JSON. I created a little Node app to work as a test harness (in test/functional) and wrote these client-side tests in ajax-test.js:
exports.testAjax = {
'test ajax get': function() {
$t.get('/get-test', {
success: function(r) {
assert.equal('{"key":"value"}', r.responseText);
}
});
},
'test ajax post': function() {
$t.post('/post-test', {
postBody: 'key=value&anotherKey=anotherValue',
success: function(r) {
assert.equal('value', r.responseText);
}
});
},
'test json post object': function() {
$t.post('/post-test', {
postBody: { key: 'value' },
success: function(r) {
assert.equal('value', r.responseText);
},
error: function() {
assert.ok(false);
}
});
},
'test json post object with application/json': function() {
$t.post('/post-test', {
postBody: '{"key":"value"}',
contentType: 'application/json',
success: function(r) {
assert.equal('value', r.responseText);
},
error: function() {
assert.ok(false);
}
});
},
'test json post array with application/json': function() {
$t.post('/post-array', {
postBody: '["value"]',
contentType: 'application/json',
success: function(r) {
assert.equal('value', r.responseText);
},
error: function() {
assert.ok(false);
}
});
},
'test json parsing': function() {
$t.post('/give-me-json', {
contentType: 'application/json',
success: function(r) {
assert.equal('value', r.responseJSON.key);
}
});
}
};
I actually wrote the tests first and tried to run them to see what happened. The first problem I found was it didn’t convert those JavaScript objects to strings and encode them for the HTTP request.
I wrote this to serialize them:
/**
* Serialize JavaScript for HTTP requests.
*
* @param {Object} object An Array or Object
* @returns {String} A string suitable for a GET or POST request
*/
net.serialize = function(object) {
if (!object) return;
var results = [];
for (var key in object) {
results.push(encodeURIComponent(key) + '=' + encodeURIComponent(object[key]));
}
return results.join('&');
};
Then in the ajax function I check the parameter’s type:
if (typeof options.postBody !== 'string') {
// Serialize JavaScript
options.postBody = net.serialize(options.postBody);
}
JSON responses from the server weren’t being parsed into responseJSON either. I wrote this very simple JSON parser based on jQuery’s implementation:
/**
* Parses JSON represented as a string.
*
* @param {String} string The original string
* @returns {Object} A JavaScript object
*/
net.parseJSON = function(string) {
if (typeof string !== 'string' || !string) return null;
string = string.trim();
return turing.detect('JSON.parse') ?
window.JSON.parse(string) :
(new Function('return ' + string))();
};
Notice this uses the capability detection code we added a few months ago. JSON parsing is detected using turing.detect('JSON.parse'):
/**
* JSON.parse support can be inferred using `turing.detect('JSON.parse')`.
*/
turing.addDetectionTest('JSON.parse', function() {
return window.JSON && window.JSON.parse;
});
The core ajax function can now use net.parseJSON:
function ajax(url, options) {
var request = xhr();
function respondToReadyState(readyState) {
if (request.readyState == 4) {
// Here:
if (request.getResponseHeader('content-type') === 'application/json')
request.responseJSON = net.parseJSON(request.responseText);
if (successfulRequest(request)) {
if (options.success) options.success(request);
} else {
if (options.error) options.error(request);
}
}
}
// etc.
The Node Ajax Test App
The Node test app is written with Express. It just serves some static files straight from the Turing distribution, and has some methods that respond to the client-side test script:
app.get('/get-test', function(req, res) {
res.send({ key: 'value' });
});
app.post('/post-test', function(req, res) {
res.send(req.body.key);
});
app.post('/post-array', function(req, res) {
res.send(req.body[0]);
});
app.post('/give-me-json', function(req, res) {
res.send({ key: 'value' });
});
Take a look at the full thing in test/functional.
Conclusion
Now the turing.net module has support for sending and receiving JSON, it’s a little bit more useful. Parsing JSON isn’t that difficult, but encoding JavaScript objects as JSON for sending over HTTP as strings is more work. I still use json2.js to do this, but maybe we should include it in Turing? Exploring the code in json2.js would certainly make for an interesting tutorial.
I think the fact our server-side tests have been written in JavaScript using Node is pretty cool — we don’t need any PHP or Ruby support to get Turing thoroughly tested.
This week’s code can be found in commit 8f75ed7.