Unit Testing Async Calls and Promises with Jasmine
Promises can often be puzzling to test due to their asynchronous nature. After looking at Jasmine documentation, you may be thinking there’s got to be a more simple way of testing promises than using setTimeout
. This post will show you a simple approach to test a JavaScript service with an exported function that returns a promise.
Unit testing is all about isolating the method that you want to test and seeing how it behaves when it takes some parameters or makes other function calls. Methods usually have dependencies on other methods, and you might get into a situation where you test different function calls within that one method. Doing so breaks encapsulation and should be avoided when possible. For any one function, all you want to determine is whether or not a function returns the expected output given a set of inputs and whether it handles errors if invalid input is provided.
With this example, we want to test the exposed fetchPlaylistsData
function in playlistsService.js
.
As you can see, the fetchPlaylistsData
function makes a function call from another service. This is where a mock comes in handy. A mock is basically a fake object or test data that takes the place of the real object in order to run examples against the spec. In Jasmine, mocks are referred as spies that allow you to retrieve certain information on the spied function such as:
- The arguments passed to the function
- What value the function returns
- How many times the spied function was called
For our unit test, we want to test if the fetchPlaylistsData
function calls fetchData
from apiService
. Before we begin writing the spec, we create a mock object that represents the data structure to be returned from the promise.
We require this at the top of our spec file:
const promisedData = require('./promisedData.json');
We’re going to use the promisedData
object in conjunction with spyOn
. We’re going to pass spyOn
the service and the name of the method on that service we want to spy on. Since we are performing an async operation, we should be returning a promise from this function. By chaining the spy with and.returnValue
, all calls to the function will return a given specific value. We are supplying it with a fake response to complete the function call on its own. Because original function returns a promise the fake return is also a promise: Promise.resolve(promisedData)
.
spyOn(apiService, 'fetchData').and.returnValue(Promise.resolve(promisedData));
Once you have the spy in place, you can test the full flow of how the fetchPlaylistsData
function, that depends on apiService.fetchData
, runs without relying on actual API responses. It’s important to note that we want to test playlistsService.fetchPlaylistsData
and not apiService.fetchData
.
apiService.fetchData
is essentially a hidden input to playlistsService.fetchPlaylistsData
which is why we fake it just like other inputs for playlistsService.fetchPlaylistsData
function call. We do not want to test API responses because they are external to our app.
You can check on the spied on function in .then
of the async call. This is where you can use toHaveBeenCalled
or toHaveBeenCalledWith
to see if it was called. You should also check if the result of the promise is the expected output you want to see via the toEqual
matcher.
expect(apiService.fetchData).toHaveBeenCalledWith(video);
expect(result).toEqual(promisedData);
Because we’re testing an async call, in your beforeEach
or it
block, don’t forget to call done
. The test runner will wait until the done()
function is called before moving to the next test.
There’s more you can do with spies like chaining it with and.callThrough
and and.callFake
when testing promises, but for the most part, that’s it!
You have a working, tested JS service! 🎉
Read more: