What is API mocking?
At Codibly, we focus on a robust core of the app by writing tests on our model layer (data fetching & transforming) in the first place.
This pattern helps us write integration tests, which rely mostly on data from the server.
- If any server call returns a 401 response, the app should try to refresh tokens and repeat the request; the user shouldn’t see the difference,
- If refreshing tokens call fails, the user should be logged out,
- If the log-in call fails and responds with “new password required” status, the user should see the screen where s/he sets a new password.
These are business-level scenarios we need to cover in our tests.
But how do we create a mock for these API calls so they return what we need them to return? We standardize the process using the following API mocking pattern:
First – define the Problem:
- Integration tests focused on the server-related state are impossible without control that comes down to mock server responses,
- Each API call can result in several different responses (which we handle differently),
- There are several ways of how we can mock data – we want to standardize on which level we do that,
- Frontend features development is often blocked until the backend is ready. We want to agree on the contract and start working concurrently with the backend team to be more time-efficient.
The API layer
In our opinionated frontend architecture, we divide our app into “layers”. One of them is “the API layer”.
Each of our domain modules contains its own set of layers, including the API.
API layer constitutes of the three main pieces:
- *.api.ts – stateless service responsible for making API calls,
- *.api.dto.ts – typescript definition types which are “the contract” describing both request and response structures of each call,
- *api.mock.ts – mocking class that can override real HTTP requests with predefined data (also allows to inject response by client).
In our apps, every domain module has its api/ folder. For example, an e-commerce application will have src/domain/cart/cart.api.ts, etc.
The essential part of this pattern is standardization. How we implement them is less important than which abstraction we use for that.
API service & its DTO
To give you some context, I will show you an example of API & DTO files before diving into a mocking file.
First, we settle the contract with the backend (e.g., based on swagger):
Then we write service for API calls:
With these simplified examples (in real life, we do some extras here like parsing non-200 errors, extracting errors, etc.), we can follow our mocking layer.
Mocking API class
The mocking class should cover each API call and provide an override of possible responses. There are few features I’d like this class to cover:
- Encapsulate endpoints – I don’t want to use them directly in my tests,
- Mock endpoint if I need it,
- Provide sane defaults of the response if I don’t provide my own,
- Allows me to inject my own response optionally,
- Expose static data fixtures for various scenarios.
Mocking tool – Axios vs. MSW
Once, we used axios-mock-adapter(npm) to mock our responses; however, after some time, we switched to msw (npm). The reason is that potentially some logic might happen somewhere in the API layer, so we can accidentally mock more than we should. With msw we make sure we only mock raw json response, so the mock is as minimal as possible. We can also use msw to run it in the browser (as a service worker) to mock endpoint not ready yet on the backend.
Using the getCart endpoint scenario, I’d like my mock to implement an interface like this:
RequestHandler is instance created by msw.rest[method]
Now, let’s implement it using msw. Keep in mind that the code is simplified and might not compile with this exact form.
When we implement something like this, we match our assumptions:
- We can mock endpoint in any test without going into details – we only need to know that we call mockSuccess() and it should work for base scenario (note that not every test where we mock cart is about testing the cart; we might want to check if any piece of our app works, but we don’t want the app to crash);
- We can get fixtures (and add more getters), in this case – a single item or many items. We can extend it with some specific scenarios (getSomeSpecificCartSet()…);
- We provide mocking methods that create handlers for our covered scenarios (error, success, and we can add more);
- We allow mapping/altering responses.
Now, we need to use mocking service in any test, like in this example:
Thanks to this flexible approach, we can extend mock to cover new cases while our API grows, and it still should work well, allowing us to alter responses (open/close principle).
There are several of them available for you to use instead of mocking API:
- Create a single API-mock file per API class,
- Try to cover mock responses when you implement new API calls,
- Try to cover each endpoint in several scenarios,
- Use API mocks to mock in tests,
- Use API mocks to develop new features ahead of the backend,
- Construct new objects, ensure they are not mutable,
- If you extract your API somewhere else (e.g., npm private package), move mocks with API service too.
- Don’t change a particular implementation if you have a working one. Don’t rewrite axios-mock-adapter if it fits you.
- Don’t try to be too generic. These classes are not meant to be reused. This pattern is only an example – if you like, you can make this object flat or make each separate endpoint class exported as a single namespace in the module.
I hope this pattern will help you with writing more testable code. At Codibly it fits our needs in terms of framework standardization.