Sinon.JS is a great library created by Christian Johansen, author of the great book, Test-Driven JavaScript Development, to make easy dealing with Stubs, Spies, and Mocks.
Although Jasmine already has support for Stubs and Spies, we are going to use a specific functionality of Sinon.JS to test AJAX requests, its FakeXMLHttpRequest
and FakeServer
functions.
The main difference between a Stub and a Fake, as you will see with the FakeXMLHttpRequest
object, is that a Fake is like a simpler but still complete implementation of a real component, and it is usually set at a system level.
Before we dig into the spec implementation, first we need to add Sinon.JS to the project. Go to http://sinonjs.org/ and download the current release, placing it inside the lib
folder.
We also need to add it to the SpecRunner.html
file, so go ahead and add another script:
<script type="text/javascript" src="lib/sinon.js"></script>
Whenever you are making AJAX requests with jQuery, under the hood it is using the XMLHttpRequest
to actually perform the request.
XMLHttpRequest
is the standard JavaScript HTTP API. Even though its name suggests that it uses XML, it supports other types of content such as JSON, the name has remained the same for compatibility reasons.
So instead of Stubbing jQuery, we could instead Fake, the global XMLHttpRequest
object. That is exactly what Sinon.JS does with its FakeXMLHttpRequest
implementation.
Let's rewrite the previous spec to use this Fake implementation:
describe("when fetched", function() { var xhr; beforeEach(function() { var fetchRequest; xhr = sinon.useFakeXMLHttpRequest(); xhr.onCreate = function (request) { fetchRequest = request; }; stock.fetch(); fetchRequest.respond( 200, { "Content-Type": "application/json" }, '{ "sharePrice": 20.13 }' ); }); afterEach(function() { xhr.restore(); }); it("should update its share price", function() { expect(stock.sharePrice).toEqual(20.13); }); });
First, we tell Sinon.JS to replace the original implementation by its Fake using the sinon.useFakeXMLHttpRequest
function.
Then, we add an observer to get the newly created requests by setting a function as a value of the xhr.onCreate
attribute, storing them on a variable named fetchRequest
.
We then invoke the stock.fetch
function, which will invoke $.getJSON
, creating a new XMLHttpRequest
under the hood.
And finally, we use the fetchRequest
variable (which contains the FakeXMLHttpRequest
object caught by the observer), to respond with a fake content.
We use the respond
function, which accepts three parameters:
Then, it's all a matter of running the expectations:
it("should update its share price", function() { expect(stock.sharePrice).toEqual(20.13); });
Since Sinon.JS changes the global XMLHttpRequest
object, you must remember to tell Sinon.JS to restore it to its original implementation after the test runs, otherwise you could interfere with the code (such as the Jasmine jQuery fixtures module) from other specs:
afterEach(function() { xhr.restore(); });
Sinon.JS's FakeXMLHttpRequest
is a very good solution to stub AJAX requests, but things can start to get complicated if you need to deal with more than one request, or need to have different responses for different requests.
To help manage FakeXMLHttpRequest
instances, Sinon.JS comes with another solution, the Fake server.
Sinon.JS Fake server abstracts the manipulation of the individual FakeXMLHttpRequest
instances into a high-level API, that lets you focus on what response you want for a particular request type.
Again, let's rewrite the same example, but now using the Fake server functionality:
describe("when fetched", function() { var xhr; beforeEach(function() { xhr = sinon.fakeServer.create(); xhr.respondWith([ 200, { "Content-Type": "application/json" }, '{ "sharePrice": 20.13 }' ]); stock.fetch(); xhr.respond(); }); afterEach(function() { xhr.restore(); }); it("should update its share price", function() { expect(stock.sharePrice).toEqual(20.13); }); });
Now, instead of dealing with XMLHttpRequest
, we create a new instance of the Fake server using the sinon.fakeServer.create
function.
Then, we call the respondWith
function to configure the Fake server to always respond to requests with a Fake response.
After the stock.fetch()
call, we tell the Fake server to respond to all made requests.
After each spec runs, it is also important to restore the original XMLHttpRequest
behavior.
The coolest thing about Fake server is its ability to create different responses, based on different URLs. For instance, we could have written the previous server response as:
xhr.respondWith(
'/stocks/AOUE',
[
200,
{ "Content-Type": "application/json" },
'{ "sharePrice": 20.13 }'
]
);
Notice the extra parameter '/stocks/AOUE'
telling the Fake server to only respond to requests made with that URL. It is even possible to specify the HTTP method (GET, POST, and so on) and to use regular expressions to match the URL:
xhr.respondWith( 'GET', //stocks/(.+)/, [ 200, { "Content-Type": "application/json" }, '{ "sharePrice": 20.13 }' ] );
You can also pass a function to the body parameter, and have dynamic responses:
xhr.respondWith( 'GET', //stocks/(.+)/, function (request, stockSymbol) { request.respond( 200, { "Content-Type": "application/json" }, '{ "sharePrice": 20.13 }' ); } );
Notice the stockSymbol
parameter, it contains the matched value extracted from the request URL based on the //stocks/(.+)/
regular expression. Whenever using regular expressions and function bodies to handle requests on a Fake server, the matched strings passed to the function in the order they are found.