Unit Testing Frontend Network Requests

Unit testing is a great way to help ensure the continuous delivery of working code over a product’s lifecycle. In client-side applications, unit testing data retrieval is especially important since using data from these asynchronous calls is at the core of what most of these apps do.

Unfortunately, asynchronous calls aren’t easy to test. Whatever tool you’re using to make your network requests (Maybe jQuery Ajax, Angular Resource, or Ember Data ), it’s almost certainly asynchronous. Asynchronous data loading is great for applications since you can display empty views and load other parts of the application while waiting for the data response to be there.

However, for unit testing, it’s more challenging.  Your app is depending on a network that doesn’t exist during tests, and the code using the data probably isn’t synchronous either.

What’s a developer to do? Don’t panic. There are several strategies for unit testing asynchronous network calls in your components.

Option 1: Use a Fake Server

Set up a fake server configured with the real API URL to respond with a test data fixture. In your test, you can synchronously wait for the mock server to be hit before making your assertion.

import sinon from 'sinon'; //http://sinonjs.org/

// jasmine test
describe('Fake server test', () => {
  let subject = {};
  it('does a server test', () => {
    let server = sinon.fakeServer.create();
  
    server.respondWith(
      'GET', '/something', 200, { 'Content-Type': 'application/json' },
                        { id: 42, name: 'The Answer' }]);

    subject = new Subject();
    server.respond(); // Process any pending requests
    
    expect(subject.get('something to test')).toBe(true);
    
    server.restore();
  });
});

Pros

  • The test simulates a complete round trip to and from the server.
  • It verifies that there are no errors before or after the data come back.

Cons

  • These tests depend on the API structure, but unit testing should isolate the item under test.
  • Setting up a fake server is verbose and requires a fair amount of both setup and cleanup.

Option 2: Directly Insert the Test Fixture Data

With this method, you inject the test data directly into the component under test, bypassing the asynchronous wait time.

// jasmine test
describe('Directly Inserted Data Test', () => {
  let subject = {};
  it('does a test', () => {
    subject.model = {id: 42, name: 'The Answer'};
    
    expect(subject.get('something to test')).toBe(true);
  });
});

Pros

  • These are fast tests that are isolated from the server and the API.
  • There’s no API dependency.
  • The isolated nature makes for a great unit test.

Cons

  • Test coverage is incomplete since the test does not verify that there are no errors while your component is waiting for data to arrive. (A common bug that will escape this test is trying to access data on an object that is undefined.)

Option 3: Use a Wrapper and Mock it

This is my favorite way to unit test network requests. Write a wrapper around the network calls that returns a promise of the network response data. Then, mock the wrapper. This is especially easy to do in ES6-style JavaScript since promises are a native type.

// jest test using jasmine-pit
describe('Mocked API Wrapper Test', () => {
  let subject = {};
  
  pit('does a mock wrapper', () => {
    let apiResponse = Promise.resolve({id: 42, name: 'The Answer'});
    ApiWrapper.getData.mockReturnValue(apiResponse);
    
    subject = new Subject();
    
    return apiResponse.then(() => expect(subject.get('something to test')).toBe(true));
  });
});

Pros

  • By mocking the wrapper with a promise, we can test the component both before and after the data is returned.
  • Because we are mocking a wrapper and not the server itself, we are not depending on the server’s exact API to conduct our small front-end unit test.
  • Setup is less verbose than setting up a fake server.

Cons

  • You need to be able to resolve test assertions asynchronously.

How to Resolve Test Assertions Asynchronously

Mocha and Jasmine Pit are tools that will allow you to return a promise from a test to resolve your assertions asynchronously. Currently, I’m using Jest for unit testing, which includes Jasmine Pit for doing asynchronous assertions. Jest mocks everything not under test by default, which makes it very simple to set up an effective test for an asynchronous call.

Most frontend unit tests should directly insert data into the test, then verify the transformations and behavior of that data. However, any component that is fetching data will benefit from at least one test that verifies its behavior before, during, and immediately after the asynchronous network request.

In my experience, the least painful way to effectively test this is to create a wrapper around my API access layer, and use promises to mock out my data response.

What are your favorite strategies for frontend unit tests?

Conversation
  • AJ Head says:

    When doing that type of testing I usually setup a Virtual Machine as the server. There are some distinct advantages to do this but it does requires time to setup.

    Advantages:

    – The “target server” is a real server. In my eyes this is the most comprehensive technique to do this in a controlled environment.

    – You have great control of the actual networking connection(s). Depending upon the virtualization hypervisor used; limits can be added to the connection’s speed, reliability and latency.

    – The “target server” can be started, restarted, mangled, folded, spindled or mutilate at will. There is no impact on any “production” systems or data.

    – Creating standard repeatable test scripts using this approach is very useful for unit testing. There are many ways to do this.

    Disadvantages:

    – This requires hypervisor software and a computer system with enough “beef” to run it. There are many hypervisors available; some free, some included with the OS, some purchased. Examples include; Bootcamp for Mac OS-X; VMware Fusion for Mac OS-X; Microsoft Virtual PC for Windows; VMware Workstation for Windows; KVM, VirtualBox,Xen and VMware Workstation for Linux. They also take a bit of effort to learn how to use.

    – The hypervisor requires a Virtual Machine to be setup. This also requires effort. An operating system and the associated server applications and data need to be created. This can be difficult if the tester/developer is not familiar with the “target system(s)” and how to install and setup. These problems can be minimized by creating “per-fabricated” Virtual Machine templates and working with the “target system” administrators to setup applications and data to mirror production systems.

  • Comments are closed.