Testing Promises Indirectly with Jest

Expected Behavior
Expected Behavior Blog
4 min readNov 13, 2019

By Tony Dewan

I recently ran into difficulty creating obvious and readable tests for React components that indirectly rely on promises. I ended up with a handy workaround that allows both native asynchronous and test-friendly synchronous promises in the same test suite. It’s not earth-shattering, but it might help you!

Background

As part of a larger project, I refactored a component that loaded data from an API using jQuery $.ajax with callbacks. I abstracted that API access into a new class (let’s call it BlogApiStore) and used promises instead of callbacks. It’s a more forward-looking approach that presents a nicer API. Here’s a simplified example showing a before and after:

Before

class Blog extends React.Component
constructor: (props)->
super(props)
@state =
posts: null
errors: []
componentDidMount: ->
$.ajax({
url: '/api/2/posts?published=true',
type: 'GET',
dataType: 'json',
contentType: 'application/json'
complete: (response, status, xhr) =>
if status == 'error'
@setState
posts: false,
errors: @state.errors.concat(["Couldn't load posts"])
else
@setState posts: response.responseJSON
render: ->
# ...

After

class Blog extends React.Component
constructor: (props)->
super(props)
@blogApiStore = new BlogApiStore()
@state =
posts: null
errors: []
componentDidMount: ->
@blogApiStore
.loadPublishedPosts()
.then (posts)=> @setState { posts: posts }
.catch (error)=> @setState {
posts: false,
errors: @state.errors.concat([error])
}
render: ->
# ...

Testing Promises Directly

Here’s an example test for the new service class:

We use Jest as our test framework, and it provides a mechanism for testing promises directly. Simply return the promise with assertions in a then method call. Our tests are set up using:

  1. SinonJS to mock network requests. Among other things, it forces the requests to be synchronous and thus makes it reasonable to assert against the results.
  2. Node assertions rather than Jest expectations, but this test is still run by Jest.
  3. Rosie for factories.
describe('loadPublishedPosts', ()=>{
test('should return a promise and call the API', ()=>{
const server = sinon.fakeServer.create();
let store = new BlogApiStore();
let mockResponse = postsFactory.buildList(3);
server.respondWith('GET', '/api/2/posts?published=true', (xhr)=>{
xhr.respond(
200,
{ "Content-Type": "application/json" },
JSON.stringify(mockResponse)
);
});
let postsPromise = loadPublishedPosts();
server.respond();
assert(typeof postsPromise.then == 'function'); return postsPromise.then((apiResponse)=>{
assert.equal(
JSON.stringify(mockResponse),
JSON.stringify(apiResponse)
);
assert.equal(server.requests.length, 1);
})
});
});

Testing Promises Indirectly

The above test is pretty reasonable and fits nicely within the scope of tools provided by Jest, but what about that original component? It now relies on promises under the hood. Here’s a preexisting test that failed after the refactor:

test('should show an error message when unable to load blog data', ()=>{
const server = sinon.fakeServer.create();
server.respondWith('GET', /.*/, (xhr)=>{
xhr.respond(500, { "Content-Type": "application/json" }, null);
});
const wrapper = mount(<Blog />);
server.respond();
wrapper.update();
const errorMsg = "There was a problem loading posts";
assert(wrapper.html().includes(errorMsg));
});

The problem is that, though the server responds synchronously, the promises are still asynchronous. That means that the component doesn’t re-render before the assertion. Put another way, it’s testing the state of the component before the network call completes. We have a few options to make this test pass:

  1. We could mock BlogApiStore instead of the network. The major problem with this approach is that we’d no longer be fully testing the interaction between the component and BlogApiStore. We want to avoid that kind of brittle test.
  2. We could also create a new promise that forces our assertions to the end of the stack. We can easily create a helper for that purpose. My main concern with that approach is that it would introduce some major cargo-culting potential. That’s exactly the kind of thing that creeps into a codebase in places it’s not needed because it sometimes magically makes tests pass.
  3. Lastly, we could force all promises the be synchronous. This is the best solution yet because it means we wouldn’t have to change our tests. There’s one major drawback: there are times when you WANT to test real, async promises.

Synchronous Promises, but Only Sometimes

There are several tools that allow you to mock promises in some way. I picked jest-mock-promise primarily because it doesn’t require anything to change in tests themselves. Instead, you can make ALL promises synchronous. But since there are times we want to use real promises in our tests, we need some way to disable mocking. Here’s what we came up with:

import JestMockPromise from 'jest-mock-promise';const nativePromise = Promise;
const syncPromise = JestMockPromise;
global.PromiseMock = class PromiseMock {
static useSyncPromises(){
global.Promise = syncPromise;
}
static useNativePromises(){
global.Promise = nativePromise;
}
};
// Default to sync promises for faster, easier tests.
PromiseMock.useSyncPromises();

It’s a simple, custom setup file, called setupPromiseMock.js. Here’s the relevant Jest config in package.json:

"jest": {
"setupFiles": [
"<rootDir>/spec/javascript/support/setupPromiseMock.js"
]
}

Now, all tests use synchronous promises by default. For those tests where we want real promises, we need only call PromiseMock.useNativePromises(). In our PostsStore example, that looks like:

describe('PostsStore', ()=>{
beforeEach(PromiseMock.useNativePromises);
//...tests
});

Simple but effective. Promises are made synchronous by default while leaving us the option of testing real, asynchronous promises when we really want to.

--

--

Expected Behavior
Expected Behavior Blog

Official account for Expected Behavior. You can tweet us at @EB.