Testing API Requests From window.fetch

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.

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:

  1. unit - test that our code provides correct arguments to window.fetch or any client libraries that wrap it

  2. functional - test that our code results in a correctly-dispatched Request and reacts appropriately to the Response

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.

Featured