Testing API Requests From window.fetch
- 6/5/2015
- ·
- #howto
- #javascript
- #sinon
- #testing
Here’s how to combine window.fetch
and sinon
stubs to test API requests.
This is the second of two parts in a miniseries on functional client-server
testing. In the first, we used sinon.js’s fakeServer
utility to test an
XMLHttpRequest
-based client. If you’re in a
hurry, skip on over to the demo project on
Github; everyone else, read on!_
If we designed a generic API for JavaScript client requests today, chances are
that it wouldn’t look like XHR. Most of the interactions we need to make with a
server can be represented simply as a request (method, path, body)
; custom
headers take care of a few more edges; and the remainder will be made using
websockets or novel browser transport layers. It’s no surprise, then, that, the
first common cases are covered first by the emerging specification for
window.fetch
.
If we designed an HTTP client API for JavaScript today, chances are it wouldn’t look like XHR.
Introduction
Simple requests made with fetch
look much like those made by any other client
library. They take a path and any request-specific options and return the
server’s (eventual) Response
as a
Promise
object:
window.fetch('/api/v1/users')
.then((res) => {
console.log(res.status);
});
Note that we’ll use ES6 syntax throughout this discussion on the assumption that
ES6 will be in widespread use by the time window.fetch
is broadly supported
(and that babel-compiled projects
are sufficient for now); back-porting to ES5-compliant code is left as an
exercise for the reader.
Supported tomorrow, but usable today
At press time window.fetch
enjoys native support in exactly zero browsers
(update: months pass and adoption is now
mixed), but the
fine folks over at github have released an XHR-based
polyfill for the current specification. It may change
before the standard is finalized. For us, though, it’s a start–using it, we can
begin using fetch
in client applications today:
$ npm install whatwg-fetch
Next, we’ll need a simple client to test. This implementation proxies
window.fetch
requests to a JSON API, employing some trivial response parsing
to type error responses and capitalize successful ones.
import 'whatwg-fetch';
function apiError (status, message) {
var err = new Error(message);
err.status = status;
return err;
}
function onAPIError (res) {
return res.json().then(function (json) {
throw apiError(res.status, json.message);
});
}
function onAPIResponse (res) {
return {
hello: json.hello.toUpperCase()
};
}
export default function client (path) {
return window.fetch(path)
.catch(onAPIError)
.then(onAPIResponse);
};
End users won’t interact with the client
directly, of course. Rather, they’ll
be interacting with it through the UI and scheduled events within a bigger web
application. While our tests will focus on the client for simplicity, the same
approaches used to test the client can also unlock higher-level tests for the
interface. If we can mimic a server response to a direct client request, we can
mimic the same response (for instance) when a user clicks a button or saves
their progress.
High-level tests
In our previous look at testing XHR-based applications, we considered client-server interactions from both the unit and functional levels:
unit - test that our code provides correct arguments to
window.fetch
or any client libraries that wrap itfunctional - test that our code results in a correctly-dispatched
Request
and reacts appropriately to theResponse
Functional tests are marginally more difficult to set up, but testing against a standard (even an emerging one) enables separation between application logic and the request layer while encouraging more valuable tests than those written at the unit level. We’ll take a similar approach here.
Testing the client
Let’s look at a simple functional test that capture’s the client’s “success” behavior.
it('formats the response correctly', () =>
client('/foobar')
.then(json => expect(json.hello).toBe('WORLD')));
Running the test as written yields a 404 when the runner is unable to GET /foobar
. In order to make it pass, we need to describe the expected behavior of
the underlying server. Instead of using an existing utility like
sinon.fakeServer
as we did with the more complex XHR API,
the design of the fetch
API is simple enough for us to mock it ourselves.
First, let’s stub window.fetch
. This serves both to cancel any outbound
requests and to let us supply our own default behavior:
beforeEach(() => {
sinon.stub(window, 'fetch');
});
afterEach(() => {
window.fetch.restore();
});
Next, we need to mock a behavior that matches the actual Response
we would
receive from a fetched request. For a simple success response from a JSON API,
we could simply write:
beforeEach(() => {
var res = new window.Response('{"hello":"world"}', {
status: 200,
headers: {
'Content-type': 'application/json'
}
});
window.fetch.returns(Promise.resolve(res));
});
Note that this behavior is synchronous–the Promise
-d Response
is resolved
immediately–but our test is written in an asynchronous style (runners
like jasmine and mocha will wait for a returned promise to resolve
before moving on to the rest of the suite). While not strictly necessary,
assuming that a fetch could resolve during a separate tick through the event
loop yields both a more flexible test and a better representation of reality.
In any case, the test client
will now encounter the resolved Response
,
apply its formatting, and turn the previously-failing spec bright green.
Tidying up
Just as with XHRs, the server behaviors mocked across a non-trivial test suite
are likely to involve some repetition. Rather than formatting each JSON response
independently, or injecting the same headers across multiple tests, it’s well
worth considering test helpers to reduce the
volume of boilerplate. For an example, we can update the jsonOk
and
jsonError
helpers used in our XHR-based tests to build Response
objects
instead:
function jsonOk (body) {
const mockResponse = new window.Response(JSON.stringify(body), {
status: 200,
headers: {
'Content-type': 'application/json'
}
});
return Promise.resolve(mockResponse);
}
function jsonError (status, body) {
const mockResponse = new window.Response(JSON.stringify(body), {
status: status,
headers: {
'Content-type': 'application/json'
}
});
return Promise.reject(mockResponse);
}
These barely scratch the surface of useful testing facilities–we might want to
match specific requests, for instance, or write helpers to describe sequences
of requests (as in an authentication flow)–but even a simple helper like
jsonOk
can reduce test setup to a nearly-trivial line:
beforeEach(() => {
window.fetch.returns(jsonOk({
hello: 'world'
}));
});
Conclusion
window.fetch
provides a more straightforward API than XMLHttpRequest
, and
it’s reflected in our tests. Instead of needing to contrive a mock with a wide
range of event states, accessors, and boutique behaviors, fetch
can be tested
with simple stubs and instances of the actual objects used in its normal
operation. There’s still a fair amount of boilerplate, which helpers can
mitigate somewhat, but the volume of “magic”–fake global objects and the
like–needed to mimic low-level behavior is significantly reduced.