AJAX and Sinon.js
Javascript really gets its strengths from creating dynamic web pages, and a necessary component of that is AJAX. AJAX stands for Asynchronous Javascript and XML, and it is a technique which uses javascript to fetch data from a web server. It happens asynchronously, which means you can continue to run javascript code while the request is still pending. The XML part was the original use of AJAX, but nowadays people use all kinds of content-types as the return value from the server. The most common is JSON (Javascript Object Notation) but HTML is often used when it’s simply some dynamic template data that should be inserted into the DOM.
AJAX Basics
XHR
XHR (XML HTTP Request) is the browser’s implementation of asynchronous data requests. It’s the "raw layer" like canvas that is part of a browser’s native library. Like canvas, it has a lot of low-level functions that are a bit painful to use, and it also has slightly different implementations across different browsers.
We aren’t going to cover XHR, as very few projects ever deal with it directly.
GETting data with $.ajax
jQuery is the ultimate library for cross-browser compatibility, and it contains some great methods for making ajax requests. Here is an example ajax call to GET data from a server:
1 2 3 |
|
In this example, if we assume a RESTful endpoint data
would be a JSON array containing a list of circles in the system, like:
1 2 3 4 5 |
|
Notice that since this is an asynchronous operation, the data is not immediately available. Instead, we pass a function to the done
method on the object returned by ajax
. The done
method will be called with the data when the ajax request is complete. There is also fail
for an error, and always
for both cases.
So, let’s make a class called Circles
whose job will be to fetch circle data from the server and then call a callback with instantiated Circle
objects. We would like to use it like this:
1 2 3 4 5 6 7 8 9 |
|
Start by making a spec file called CirclesSpec.js
in the spec/
directory. In here, write:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
Let’s walk through this spec. First, we are describing our new object, Circles
. Next, we have our it
test for fetching against our server. Then, we’re making a callback spy. If you glance forward to line 12, you can see this is the callback we’re going to call fetch
with.
Lines 4-10 setup the fake data we’ll respond with and set up the spy on $.ajax
to return an object with done
on it that calls the given callback with the data we provided.
After we call the fetch
method, we start asserting. We check that $.ajax
was called with the proper url first. Then we get the callback spy’s most recent call so we can see what instantiated circles were passed back.
Then, we check both circles and their attributes against the data we "returned" from the ajax call.
On your own, implement Circles.fetch
to pass this test. Don’t forget to add script tags to the html file to include the spec file, your source file, and jQuery (via google’s ajax apis with http:// on the front).
Faking a server with Sinon.js
So, what do we think of the previous test and the solution? It certainly is doing a good job testing the fetch
function on Circles
, but it is also very verbose and spends most of its time setting up the relationship with $.ajax
. On top of that, it is highly coupled with the implementation.
jQuery is a very flexible library, and instead of using $.ajax(url).done(callback)
, we could have written:
1 2 3 |
|
And it would work perfectly well, but our test would fail completely. This is where Sinon steps in. Jasmine’s spies are written to work with simple Javascript objects and functions, but Sinon takes it a step further.
Let’s walk through writing a Jasmine test with a fake Sinon server.
First, download Sinon from http://sinonjs.org/.
Next, let’s setup our test scaffold:
1 2 3 4 5 6 7 8 |
|
Next, inside the describe
we will set up our data as before, along with a variable to keep track of our fake server:
1 2 3 4 5 6 7 8 |
|
When we start our test, we want to setup sinon’s fake server, and when we end our test, we want to restore the original implementation:
1 2 3 4 5 6 7 |
|
Inside our it
function, first we setup the sinon server to respond to our expected request:
1 2 3 |
|
This tells sinon to respond to a GET request at the /circles
url with a 200 (success). It should also add the response header for Content-Type
that signifies JSON data. Then, the data returned should be a JSON string of our data
variable. So, sinon will send our data
back to anyone who fetches data from /circles
.
Next, we will setup a jasmine spy as our callback and fetch from the server:
1 2 3 4 |
|
We have to tell the sinon server to respond to any pending ajax requests at this point, and then we will check to see that our callback has been called.
Last, we can grab the results of the callback and compare them with our data to ensure the fetch
method processed the data correctly:
1 2 3 4 5 6 7 8 9 10 |
|
That makes sure that the x, y, and radius in our data matches our circles.
Run this test, and it should pass. Note that since your previous test was faking out $.ajax
, you will have to remove that test (or at least commented it out) so that $.ajax
reaches the Sinon server.
Try a few variations of jQuery’s ajax functions:
1 2 3 4 5 6 |
|
Do they all work?
A Quick Aside: Jasmine Matchers
One thing we’d like to check in our previous test is that Circles.fetch()
actually gives us Circle
objects back. A simple test in javascript would look like this:
1 2 3 |
|
So, we could write this in Jasmine:
1
|
|
But it would be way cleaner if we wrote this:
1
|
|
To do that, we have to write a custom matcher. Add a SpecHelper.js
file in /spec
and include it in our test suite with a <script>
tag. Then, add this to SpecHelper.js
:
1 2 3 4 5 6 7 8 |
|
Modify the contents of the toBeA
function to return whether or not the object (this.actual
) is an instance of the class (expectedClass
).
Now, add in our check in our test:
1 2 |
|
Error handling and jQuery Deferred Objects
In our previous test, we had our fetch
method take a callback as a parameter. This is pretty common in Javascript, but it’s not particularly flexible. What would have happened if the ajax request failed?
$.Deferred()
gives you a jQuery Deferred object, and it allows you to asynchronously call different functions in different scenarios. For example, we could implement fetch
like this:
1 2 3 4 5 6 7 8 9 10 11 |
|
That would mean that we could use our fetch
function just like how $.ajax
works:
1 2 3 4 5 |
|
Modify your fetch
function to use a deferred object. Next, add another test that tests when the ajax request fails. HINT: one possible status code you could use is 404, which means the resource could not be found. This test should make sure that the fail
callback is called.
POSTing data
Next, let’s POST data to our server to create a new circle. The REST action for creating a circle is to issue a POST request to /circles
with the data for the circle. So, we’ll setup a test that expects a POST to /circles
when Circles.create()
is called.
1 2 3 4 5 6 7 8 |
|
Setup the rest of the test on your own. Implement Circles.create
to use a deferred object. HINT: $.ajax
and its derivatives return a deferred object.
Updating a circle
Let’s say we want to update a circle on the server. The RESTful request would be:
1
|
|
Where 4
is the id
of the circle.
On your own, take the following steps:
- Add an
id
parameter to the constructor forCircle
, do this via BDD in yourCircleSpec.js
- Add a test in
CircleSpec
to test that aCircle
has asave
method that makes an AJAX request to the proper url (/circles/id-of-circle
). - Implement the
save
method onCircle
to pass the test. HINT: there is no$.put
, so you need to use$.ajax(url, {type: "PUT"})
. - Add a test that ensures that the
fail
callback is run if the server rejects the update. 422 is the status code forunprocessable entity
.
Deleting a Circle
To delete a circle, the RESTful request is:
1
|
|
On your own:
- Add a test to
CircleSpec
to test that callingdelete
on a circle makes the appropriate ajax request - Implement
delete
. HINT: like update, there is no$.delete
.
Refactor
Now that we have a great test suite to fall back on, let’s clean up our code. Here are some examples:
- Implement
url
onCircle
to generate its url for use insave
anddelete
- Implement
url
onCircles
for fetch, and then use that url inCircle
’surl
as the root - Change the constructor for
Circle
to take a single argument: an object, with keys for x, y, radius, and id. You’ll have to change your tests for this, so do that first! - Make a test helper function that easily sets up a RESTful server fake and callback. For example, it could be used like this:
1 2 3 4 5 6 |
|
The fakeRest
method will setup the server and callback, then send the callback to the passed in function to be hooked up to the code being tested, then after the code being tested it has the server respond and asserts the callback is called. Rewrite your previous tests to use this helper.