DEV Community

Cover image for API Mocking Strategies for JavaScript Applications
Artem Zakharchenko
Artem Zakharchenko

Posted on

API Mocking Strategies for JavaScript Applications

API mocking is easily the most common kind of mocking in JavaScript (apart from the mocking about how there's a new framework every day). No matter what application you're building, it is likely to make some HTTP calls.

So why do you even need mocking? A good question, perhaps you don't. However, if you wish to test the code you write, you'd also have to test the code that does HTTP requests. Making actual requests in your tests is a sure path to flaky and unreliable tests, and here's where API mocking comes in.

The Request's Journey

Before we rush into code, let's stop for a moment and think about what we're trying to achieve here.

Mocking is the action of substituting a piece of software with another, seemingly compatible piece. Since we wish to mock an API call, we will be substituting some piece of the logic responsible for making that call. To better understand which part and why we'll be mocking, we need to visualize a journey of the request first.

Here's a high-level overview of what happens when your application makes a request:

The request's journey

  1. Your application calls a request client to make a request. This can be window.fetch, react-query, or Apollo.
  2. The request client forms a proper request based on your input (resource path, query parameters, headers, body, etc.), and sends it over HTTP to the server.
  3. The server receives a request and returns a response.
  4. Your application receives the response.

It's important to mention that this diagram may differ based on the application architecture you have. However, there always will be these fundamental pieces: a logic that dispatches a request and a server that processes that request.

This overview gives us some visual clues towards what parts conclude a request. We will be mocking one of those parts to achieve our goal. Now to decide which one...

API mocking strategies

I've been honored to speak about various API mocking strategies at this year's TestJS Summit conference. In the video below you can get a grasp over mocking as a technique, its purpose, and the most common (and the most efficient) ways to achieve it.

Let's take a closer look at each strategy in the written form today.

Out of the request's journey components (application, request client, server) we surely don't wish to mock an application. That's the code we're trying to test, and mocking it would be, well, pointless.

This leaves us with the two remaining parts, each representing a mocking strategy:

  1. Mocking the request client.
  2. Mocking the server.

Both these strategies are applicable and widely used. Just as any decision in programming, choosing any of these strategies comes with benefits and drawbacks. Let's focus on those.

Mocking the request client

When you mock the request client, you're making the following change in the request's journey:

The request journey altered by the mocking request client strategy

By mocking (substituting) the request client, the actual request client is taken out of the equation. A compatible client is placed in its stead, giving us the desired power to mock the responses our application receives.

The most basic example that illustrates this mocking strategy in practice is when you stub the window.fetch function:

window.fetch = (init, input) => {
  // Respond with a mocked response
  // any time our code calls "window.fetch".
  return new Response('hello')
}
Enter fullscreen mode Exit fullscreen mode

Of course, most of the time you would use a third-party library that abstracts this logic for you. Some of the prominent libraries for this strategy are:

Benefits

This strategy gives us control over the requests/responses at the earliest stage of their flow. The cost of such control is also minimal, as the mocking logic lives entirely in the client-side code, next to our application.

Drawbacks

If you take a closer look at the diagram altered by this strategy, you'll notice that not only the "Server" part is blackened out, but also the "Request" part. That is because replacing the request client means it never actually makes a request. If a request "leaves" the client, it won't be able to control it anymore.

There's also a behavioral divergence for your application: it does a request in production, but it doesn't in the tests.

Mocking the server

Alternatively, we can replace the "server" part of the request journey to make it look like this:

Mocking server strategy

Since our request client is configured to communicate with a production server, such request destination detour often happens by introducing some sort of conditional URL logic:

const IS_TEST = process.env.NODE_ENV === 'test'

fetch(
  IS_TEST
    // Communicate with a local mock server while testing.
    ? 'http://localhost:3000/api'
    : 'https://api.backend.com'
  )
Enter fullscreen mode Exit fullscreen mode

Some of the most prominent libraries for this mocking strategy are:

Benefits

With the mocking server strategy, we're allowing the request client to fully execute because it's the server part that we're placing. This makes our application behavior under test almost idenctical to the one in production. Almost.

Drawbacks

For this strategy to work, our request client must know when and how to decide which server endpoint to use. Regardless of the technical details of this strategy, it means that the requests are hitting an entirely different server. Introducing any kind of deviation puts the logic you're resting at risk. Consider this:

fetch(
  IS_TEST
    ? 'http://localhost:3000/api'
    : 'hts://apibackendcom' // Ehm, is this okay?
  )
Enter fullscreen mode Exit fullscreen mode

The actual production server URL is corrupted in the example above. Guess what, the tests would still pass while the application would be successfully broken for your users.

There is also a minor operational hassle, as you need to spawn and terminate the local mocking server before/after your test suites. You must ensure the mocking server's operability so that there are no unhandled exceptions that may fail your tests, resulting in false negatives.

Overall, introducing an entire server for the sake of mocking may be considered an overhaul. Even if you're using it for this specific purpose, it's still an actual server you need to write and maintain, increasing the operational cost of this setup.

Using third-party libraries often abstracts the operational aspect of the mocking server, so you're not experiencing it directly. That doesn't mean that it's magically gone.

Which strategy to choose?

The mocking strategy you choose largely depends on the environment where you wish to mock API. Certain environments, such as the browser, allow you to intercept requests on the network level via Service Worker API. That way you compromise on neither request client nor server, allowing all your logic to execute, hit the same production endpoints, and receive the mocked response you're in charge of.

Libraries like Mock Service Worker leverage this unique browser ability gracefully to provision seamless API mocking. Let me know if you would like to learn more about this unusual strategy in the comments below!

There are, however, environments that don't have a designated API to intercept outgoing requests. Running tests in Node.js, for example, would likely require your mocking setup to stub request issuing modules (such as http.request) in order to know what requests are happening, and mock their responses.


Afterword

Regardless of which API mocking strategy you end up choosing, please remember that:

  • The less test-specific setup you have, the better;
  • The less your app's behavior changes for the sake of tests, the better;
  • The closer to the server your request interception is, the better.

I hope you've enjoyed this analysis of different API mocking strategies you may adopt. Make sure to follow me on Twitter to stay in touch with the articles I write. Stay productive!

Top comments (2)

Collapse
 
mrtnvh profile image
Maarten Van Hoof

Great post. Thanks for sharing. I'd also like to mention, if you have a OpenAPI/Swagger document to your disposal, you should consider looking in to Stoplight's Prism. It's a tool that automagically creates an entire mocking server, incl request validation etc, based on the given OpenAPI document. It has saved me countless hours!

Collapse
 
kettanaito profile image
Artem Zakharchenko

That's a great suggestion, Maarten! I suppose there are multiple ways to spawn a local mocking server, including deriving it from some sort of API specification.