Overview
In this 3rd post in this series we are going to look at writing our first asynchronous test that test code which calls the server.
NOTE: the starting point code for this post is located here: https://github.com/RyanAtViceSoftware/react-testing-best-practices/tree/step-3-our-first-test
Writing the Test
Let’s start by adding the test below to our app.test.js
file.
describe('Given we load our app ', () => {
// other tests removed for clarity
describe('When we have posts on the server ', () => {
it('Then we get posts written to the screen',
done => {
http.get = sinon.stub();
http.get
.withArgs('/posts')
.returns(Promise.resolve(getDummyPosts()));
const app = mount(<App/>);
expect(app.getElements().length).toBeTruthy();
setTimeout(() => {
app.update();
const posts = app.find('li');
expect(posts.getElements().length)
.toBe(3);
done();
});
});
});
});
Then in the same file lets update our imports too add what’s shown below.
import App, { http } from './App';
import sinon from 'sinon';
Finally add the getDummyPost()
helper function below that will return dummy data for the test to use.
function getDummyPosts() {
return [
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
},
{
"userId": 1,
"id": 2,
"title": "qui est esse",
"body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
},
{
"userId": 1,
"id": 3,
"title": "ea molestias quasi exercitationem repellat qui ipsa sit aut",
"body": "et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut ad\nvoluptatem doloribus vel accusantium quis pariatur\nmolestiae porro eius odio et labore et velit aut"
}
];
}
Now save app.test.js
and run it and you should see output like below.
As we can see now we have a new test that is nested one more level deep using Gherkin Syntax. We now have our first When clause and we are now testing the use case below:
- Give we load our app
- When we have posts on the server
- Then we get posts written to the screen
- When we have posts on the server
NOTE:Using Gherkin is just a recommendation here but I like Gherkin because it’s very easy to be consistent and to make the scenario under test extremely clear. Here we can clearly understand that we are testing that when we load the app if there are posts on the server then they are displayed to the user.
How It Works
The key to this approach is how we are stubbing the http.get
method. First thing we do is import http
object by updating the import as shown below.
import App, { http } from './App';
Because import is a CommonJs implementation when we use a named import in this way we get access to a reference to the singleton instance of the http
object that is shared by our SUT (the App
component). Because we have a reference to the same instance of the http
object we can easily stub out methods on http
by using the code shown below.
http.get = sinon.stub();
Now our SUT code will use the sinon.stub()
version of the http.get()
method.
NOTE: Sinon is a Javascript testing library provides testing helpers like stubs and mocks.
Here we are replacing http.get
with a stub that we can then configure to return our dummy data only when it’s called with /posts
url using the code below.
http.get.withArgs('/posts').returns(Promise.resolve(getDummyPosts()));
Note that we are using Promise.resolve() which will return a new promise that will immediately resolve and return the value we pass to it. In our case we are passing in the results of calling the test version of the getDummyPosts()
function.
Now that we have stubbed http.get()
we can mount our app as we did in our last test and verify that we get a ReactWrapper
back from Enzyme as shown below.
const app = mount(<App/>);
expect(app.getElements().length).toBeTruthy();
How Do We Wait for the Async Code to Complete
At this point in our test we have to figure out a way to make our test code wait for the promise to resolve in our SUT that calls the server to get postsropse. Currently we are faking the server call using a Promise as shown below.
export const http = {
get: (url, { dummyData } = {}) =>
new Promise(resolve =>
setTimeout(() => {
resolve(dummyData);
}, 1000
)
)
};
Note that I always add a wrapper around my AJAX lib like this http
object in the projects I architect as it allows for:
- stubbing out APIs when needed
- having a simulated delay for stubbed APIs so you can test, both manually and automated, for both async spinners and other complex UI interactions
- it provides an architectural seam over your AJAX lib allowing you to swap it out if need and also allowing for standardizing things like error handling, logging, etc…
However, Javascript Promises have this quality to them as described on Mozilla.
- Callbacks will never be called before the completion of the current run of the JavaScript event loop.
What this means for our testing code is now we have to make any test code that comes after we mount our App
component wait for our stubbed http.get()
call’s promise to resolve. There is a lot of background information to understand the approach we are going to follow here and I recommend Daniel Parker’s excellent Javascript Promises book as a primer on promises and the Javascript Event Loop if you are new to them.
However, here we will provided a simplified explanation. Basically each time we call setTimeout()
or use a promise the the callback code that we configure to be called will go to the end of the Javascript’s event loop’s queue for processing. In our code when the CUT calls http.get()
our test code is already executing which means that our test code will run to completion before any callbacks are called so we can’t just put the code that would expect certain results after the async call returns right after our other test code as it would run before our async call returns. So what we have to do is push our test code that we want to run after any pending async callbacks to the bottom of the event loop’s queue. The easiest way I know to do this is using setTimeout()
and passing 0 for the second parameter (which is the default value so we can omit the second parameter) as shown below.
setTimeout(() => {
app.update();
const posts = app.find('li');
expect(posts.getElements().length)
.toBe(3);
done();
});
Now our code from like 2 to 9 will be executed as the last thing in the event loop’s queue meaning our test code will wait for the CUT’s callbacks to fire before continuing.
The last piece of the puzzle here is getting Jest to wait for our async callbacks to complete and forturnately Jest supports async test. We do this by calling the done()
method that will be passed as the first argument Jest’s it
method as shown on line 9 above.
NOTE: the code for this point in this post is located here: https://github.com/RyanAtViceSoftware/react-testing-best-practices/tree/step-3-our-first-test
How Do We Know This Will Work with Code That Really Calls the Api
The easiest way to know this will work with the real code that calls the API is to add the real code and see if our goal of not having to change tests is achieved.
First lets run npm install axios --save
to install axios which is a small library we can use for making AJAX calls to the server.
Next let’s update our http.get()
call as shown below.
const baseUrl = `http://jsonplaceholder.typicode.com`;
export const http = {
get: (url, config, { dummyData } = {}) =>
dummyData ?
new Promise(resolve =>
setTimeout(() => {
resolve(dummyData);
}, 1000
)
)
: axios.get(baseUrl + url, config).then(r => r.data)
};
What we have done here is to check to see if we have dummyData
on line 5 and if we do then the code is the same as before and we fake the async call using the setTimeout()
method and return dummyData
. However, if we don’t specify dummyData
then we will make a live call using axios.get()
passing along the url
and config
that was passed into us. We are also prepending baseUrl
which is declared immediately above http
on line 1 above.
NOTE: in a real application we would extract all the http code and the configurations like the base url to separate files\modules.
The last thing we have to do to get this working is update our call to http.get()
so that we are no longer passing in {dummyData: getDummyPosts()}
which will cause us to call the live service as shown below.
function getPosts() {
return http.get('/posts'); // we removed {dummyData: getDummyPosts()}
}
If we run the code now we will see that the web app is displaying all the posts from the server as shown below.
And if we rerun the tests we will see that all tests are still passing.