Mocking External Services in Jest — The Easy Way

Apr 26, 2020

Contributor

Jake Grafenstein avatar

Jake Grafenstein

Author

Full-stack developer passionate about clean code and effective testing strategies.


In the world of modern web applications and microservice architecture, chances are your application makes calls to an external service. Maybe you're storing user data in a cloud database such as Firestore, uploading photos to a media platform like Cloudinary, or connecting your application to a service that you've developed to keep your modules decoupled (keep being awesome, you 21st century developer, you).

Using these tools is great, and totally increases your productivity… until it comes time to start writing tests. It's always the bane of every creative developer. You may be tempted to write full-stack integration tests using WebDriver, but these sorts of tests are expensive to run, difficult to maintain, and fragile! What's a poor developer to do?

Unit tests to the rescue!

At Dashboi, we're using Firestore as a cloud database. Because this service is built by Google and I trust their quality, I don't feel the need to test that part of the stack using automation.

I still want to make sure everything about my application works up until the point that I integrate with the service. To do that, I write tests using Jest.

So how do you deal with code paths that interact with your services? One might try to mock the service. Jest has a nifty feature called Manual Mocks that can be used to mock a module, which can be very useful if the module you're trying to mock is simple. For the uninitiated, the term mocking in this context means to provide a stubbed out version of the function or service that is considerably simpler, and allows you to verify the logic around the function call.

The problem with Manual Mocks

The example that they give in the Jest docs is mocking the file system module, which is a great example except it's incredibly simple. The file system module has a straightforward interface.

Try mocking a service like firestore, and you're in for a real headache. Not only are there practically no resources on the web for it (hence, why I'm writing this article), it's also just an incredibly complicated module that will take a lot of effort to reconstruct.

Another reason that mocking a module is a bad idea is that you're essentially mocking the module at a specific point in time. Should you ever try to update the module, you will most likely have to rewrite your mock implementation.

There has to be a simpler way to do this.

A concrete example

To be a bit more concrete about the problem we're trying to solve here, imagine I have a function call to write to the firestore database like this:

export function sendFeedback({ uid, sentimentScore, title, body }) {
return (dispatch) => {
db.collection("feedback")
.add({ uid, sentimentScore, title, body })
.then((docRef) => dispatch({ type: SEND_FEEDBACK_SUCCESS }))
.catch((err) => dispatch({ type: SEND_FEEDBACK_FAILURE }));
};
}

If you don't understand everything that's going on here, don't worry, This is an example of an Action Creator from Redux. The important piece we'll talk about is the call to db.collection("feedback"), which is a call to our external service, firestore.

The problem here is that the code is embedded deep within an action creator. To test this, we'd need to create a manual mock of the db object within the firestore module, which is pretty tough to do. We don't want to mock the function call to sendFeedback() because it's a part of our redux stack, part of the logic that we're trying to verify.

The solution

The solution is a simple one, and once you hear it, you're going to bang your head against the table for not thinking of it yourself:

  1. Take all the code that interacts with the service, and put it into a separate file. This will be your service abstraction layer. This file will have single responsibility. It contains no application logic.
  2. Refactor your existing code to depend on the new file instead of the service directly.
  3. Create a mock implementation of the abstraction layer. You control the inputs and outputs of the mocked function calls.
  4. Set up your jest tests to use the mocked version of the service instead of the real one.

And that's it! Here's what it looks like in action.

Abstraction Layer

export function writeFeedback({ uid, sentimentScore, title, body }) {
return new Promise(function (resolve, reject) {
db.collection("feedback")
.add({ uid, sentimentScore, title, body })
.then((docRef) => resolve(docRef))
.catch((err) => reject(err));
});
}

Because the call to db.collection returns a promise, so too must our abstraction layer, otherwise the code will behave as if it is synchronous and cause us bugs down the line. Notice how all this function does is set up a promise, call the asynchronous db method, and then either resolve the document reference that is returned or reject any error that is encountered.

Code Refactor

export function sendFeedback({ uid, sentimentScore, title, body }) {
return (dispatch) => {
writeFeedback({ uid, sentimentScore, title, body })
.then((docRef) => {
dispatch({ type: SEND_FEEDBACK_SUCCESS });
})
.catch((err) => {
dispatch({ type: SEND_FEEDBACK_FAILURE });
});
};
}

Next, we have to refactor our existing code to use the new abstraction layer. Notice how nothing has changed except we are calling writeFeedback instead of db.collection. If you do this correctly, your refactoring should look as simple as this.

Create a Mock Implementation of the Abstraction Layer

This is where the fun begins. As stated in the Manual Mocks user manual, this is an example of mocking a user module. Our module is the abstraction layer we created in step one. To do this, create a __mocks__ directory inside of the directory that holds your abstraction file. Your file structure would look something like this:

📂 db
↳ 🗄feedbackAbstraction.js
↳ 📂 __mocks__
↳ 🗄feedbackAbstraction.js

Inside the db abstraction folder, we have a file called feedbackAbstraction.js which holds all of the logic for interacting with the database for our feedback code. Then, we have a directory that contains the mocked implementation of our feedback code. Note: these two files must be named the same thing.

export function writeFeedback({ uid, widgetList, layout, numberOfColumns }) {
return new Promise(function (resolve, reject) {
if (uid === "a100") {
resolve({ id: 1 });
} else {
reject("an error occurred");
}
});
}

This is the mocked implementation of the writeFeedback function. Because I control the logic, I can make up whatever rules I want in my tests. Here, I've decided that if I pass in the uid a100, the mocked function should return as if the feedback was written successfully to the db. Otherwise, it will act as if there was an error. This allows me to test the different paths in my application logic.

Use the mocked implementation in your tests

The last part is easy. When writing your jest tests, should you ever want to use the mocked implementation of the service instead of the real one, all you need to do is include this one-liner at the top of your test file:

jest.mock("./db/feedbackAbstraction.js");

Doing so will tell jest to look in the __mocks__ subdirectory of db and use the implementation it finds there.

Conclusion

To wrap things up, the most important part about mocking services is you should create an abstraction layer for the service that contains no application logic and then mock that layer instead of trying to mock the service itself. Doing so will save you a lot of time, effort, and just maybe, your sanity.

Thanks for reading!


Originally published on Medium