Click here to Skip to main content
15,887,585 members
Articles / All Topics

Snapshot Testing APIs with Jest

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
19 Sep 2017CPOL4 min read 6.8K   1  
Snapshot Testing APIs with Jest

You know what’s annoying? API mismatches.

One day, the backend devs change one of the APIs without warning the frontend devs. “We decided dateCreated was a better name than created_at,” they say. “Didn’t we tell you in standup yesterday?”

And then, everything is broken.

There are unit tests covering the UI code. There are unit tests covering the backend code. All of those are passing. And yet, the app is broken.

In this post, we’ll cover how you can write API tests with Jest, with very little code, and avoid this mess.

Not End-to-End Testing

What’s missing is a set of tests that check that the frontend and backend are integrated correctly.

These are called end-to-end or acceptance tests, and they’re typically done at the browser level. A tool like Selenium or Nightwatch or Capybara drives a headless browser to log in, click around, fill out forms, and generally ensure that everything is working correctly.

There are a few problems with end-to-end (E2E) tests though – they’re slow, error-prone, and brittle. Selenium-style browser automation is tricky to write well. Sneaky timing bugs can creep in, causing tests to fail intermittently.

If a test says:

Load the user profile page and assert that the <h2> tag has the text User Profile

If you then go and change it to an <h3>, the test will fail.

So there is a fine balancing act in writing tests like this – you need assertions that are good enough to verify functionality, but not so specific that they break when you introduce an extra <div> or something.

A Happy Medium: Snapshot API Tests

The Jest tool from Facebook supports a style of testing called snapshot testing, where basically:

  1. You manually verify that the code works.
  2. You write a snapshot test and run it. It saves a text representation of the thing. You check the snapshot into source control.
  3. After that, every time the test runs, it verifies the result against the old snapshot on disk. If they don’t match, the test fails.

This is typically applied to React components (and you can read about snapshot testing React components here), but snapshots can be taken of anything. Any JS object can be snapshotted.

Which means, you can:

  1. Make an API call
  2. Snapshot the result
  3. Sleep well knowing that if the API snapshots are passing, your UI and backend are in agreement

Considerations

If you’ve written unit tests before, you’ve likely mocked out your API so that it doesn’t make any calls. In these tests, we’re turning that on its head. We want to make real API calls against a real server.

This means you will need a backend server running in order to run these tests. It’s a bit more complexity, but in trade, you get a bit more confidence.

You also need to be aware of the test database, and be sure to reset it to a known state before you do something like “Create 3 transactions, and verify that GET /transactions returns 3 transactions.” Run that twice without cleaning the database, and the test will fail.

I won’t go into depth here about how to set all this up, because it will depend heavily on your own backend setup, your CI setup, etc.

If you decide to try this out, start simple: write tests against things like “login” or “create” that will be resilient to a dirty database. If you find you like the approach, then you can worry about solving the problems of database/CI/etc.

Examples

Testing Login

Here are a few tests of a theoretical “login” service:

JavaScript
import * as API from 'api';

test('failed login (bad password)', async () => {
  let data;
  try {
    data = await API.login('me@example.com', 'wrong_password');
    fail();
  } catch(e) {
    expect(e.response.data.error).toMatchSnapshot();
  }
});

test('failed login (bad username)', async () => {
  let data;
  try {
    data = await API.login('not-a-real-account@example.com', 'password');
    fail();
  } catch(e) {
    expect(e.response.data.error).toMatchSnapshot();
  }
});

test('good login', async () => {
  try {
    const response = await API.login('test-account@example.com', 'supersecret!');
    expect(response).toMatchSnapshot();
  } catch(e) {
    fail();
  }
});

These tests take advantage of async/await to make the code read more like synchronous code.

There’s not too much magic happening here: each test makes an API call, and asserts that the result (or error) matches the snapshot.

Remember, you have to verify that the API calls are working before you run the snapshot tests for the first time. They’re typically saved in a __snapshots__ folder alongside the test JS file, so you can inspect them for correctness as well (and you should).

Testing Things that Change

Sometimes the API responses might contain an auto-incremented ID, or a timestamp. These things will cause a snapshot test to fail every time.

To fix that, here is an example of a sanitize function that takes an object, and an array of keys to sanitize. Since it uses lodash’s set function, the keys can reference “deep” properties like user.orders[0].created_at if necessary.

JavaScript
import * as _ from 'lodash';
import * as API from 'api';

function sanitize(data, keys) {
  return keys.reduce((result, key) => {
    const val = _.get(result, key);
    if(!val || _.isArray(val) || _.isObject(val)) {
      return result;
    } else {
      return _.set(_.cloneDeep(result), key, '[SANITIZED]');
    }
  }, data);
}

test('createOrder', async () => {
  let order = await API.createOrder('Camera', 47, 19.84);
  order = sanitize(order, ['id', 'created_at']);
  expect(order).toMatchSnapshot();
});
</code>

Try It Out

I’ve only just started to implement this testing approach in my own projects, but it seems promising so far. Give it a try, and leave a comment if you do. :)

Snapshot Testing APIs with Jest was originally published by Dave Ceddia at Dave Ceddia on September 18, 2017.

This article was originally posted at https://daveceddia.com/feed.xml

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
Dave is a Software Engineer in the Boston area and writes about AngularJS and other JavaScript things over at daveceddia.com

Comments and Discussions

 
-- There are no messages in this forum --