#
Mocking API handlers and data
Mock API handlers and data should be managed with care. While mocks are indispensable in allowing us to run and test our applications without relying on API availability, we don't want to spend unnecessary time creating and maintaining them.
Mocks that attempt to do too much can quickly snowball into a full implementation of our APIs in the browser. This can become excessively time consuming as well as introduce incorrect results, tests, and assumptions later on as requirements, backend API functionality, and data relationships change.
We want to find a balance between providing mocks that work well enough to serve our purposes while avoiding costly, time-consuming maintenance and other potential issues.
The following guidelines will help you to keep the scope of our mocks within reason and prevent the occurrence of common issues associated with mocks that attempt to do too much.
#
1. Return all sub-types from the same source
For each parent type and all of its derived types, there should be only one set of mocks.
In most data sets and APIs there exist types that share all of their properties with a common parent type. This entity may or may not actually exist or be retrievable from a single data source or API, but rather, serves as an idea or representation of a broader idea or what we might call and enterprise data type.
All mock handlers within a single application that return any of these derived types should use data from the same source collection of mocks. This implies that the source collection should fulfill the smallest possible interface that meets the requirements of each type that uses it and that mock handlers will return a wider interface than any single consumer may require.
If these sub-types aren't compatible with the parent type or with one another, please review the section on keeping property names consistent across use cases in the api standards and work with a backend developer to resolve the conflict if possible.
#
Example
Consider the following types:
type UserDetail = {
ref: number;
firstName: string;
lastName: string;
fullName: string;
email: string;
department: string;
title: string;
createdOn: string;
updatedOn: string;
};
type UserSummary = Pick<UserDetail, "ref" | "firstName" | "lastName">;
Given the following endpoints:
All of the mock handlers for these endpoints should be covered by a single array of mocked data that fulfills the UserDetail interface. There is no need to maintain a set of mocks for each of them. Mock handlers returning an interface representing a subset of its parent type should simply return the parent type because it fulfills the narrower interface.
#
2. Searching, filtering, and sorting mocked data
The previous section alluded to the possibility that an object can be selected from a collection and returned from a mock handler that intercepts a request for a single item.
For example, when handling a request such as GET /user/:userRef it is perfectly acceptable to search the mocked users collection with mockedUsers.find(u => u.userRef === params.userRef) and return the user if found or a 404 otherwise.
It is also acceptable to return sub-collections of items attached directly to a mocked object to handle a request such as GET /application/:applicationRef/role, this is covered in more detail in the section covering dynamic mocks.
Any further handling of the searching, filtering, and/or sorting of mocked data should be avoided. Our UIs communicate the intent and state of these types of actions via query parameters in the URL which are then processed and sent alongside requests to the API and that is the end of their responsibility.
Attempts to recreate this behavior in your mocks will lead to complex, potentially buggy, and difficult to maintain code that is completely unnecessary for our purposes when working with mocked data. The goal is to get data on the screen and test UI behavior, not API functionality and relationships between data.
#
3. Dynamic mocks
Generally speaking, mocks should be as static as possible at both start and run time. Dynamic mocks in this context do not refer to mutations applied via mock handlers. Mutations, covered in a later section, are acceptable and encouraged in many scenarios. Here, dynamic mocks refer to the creation of mock data at runtime as well as the manipulation of properties when a request is intercepted by a mock handler.
#
Avoid dynamically mutating mocks in handler responses
Many mocked data types will have a property that represents a foreign key relationship to another type. Because we maintain a single set of mocks for any given set of common types, when a request is intercepted it can be tempting to manipulate the foreign key of these mocks into matching the identity of the requested parent resource.
#
Example
type Application = {
applicationRef: number;
name: string;
description: string;
roles: Role[];
};
type Role = {
roleRef: number;
applicationRef: number; // FK to Application.applicationRef
name: string;
description: string;
};
In a mock handler that serves the response to GET /application/:applicationRef/role you may want to map over the set of mocked roles and update the applicationRef field to match that of the URL path argument. However, the UI never cares whether or not this relationship is correct and this effort is wasted and adds unnecessary complexity. Simply return the collection of mocked roles as they are and move on.
#
Mocks generated at start time
In the example above, an application can have roles associated with it and these roles are unique to each individual application. Instead of returning or manipulating the same set of roles when a request is intercepted for any application, the mocked application objects can be modified at start time to include roles built from a random or selective subset of the default collection for mocked roles through the use of helper functions such as pickRandomSubset(arr: T[], amount: number): T[] or manual insertion [mockRoles[1], mockRoles[3]]. Mock handlers that retrieve and mutate these collections of items can then retrieve the parent first and handle the request from there.
Mocks that are generated in this way are acceptable only when working with a direct, one-to-one relationship established with foreign keys. It is not acceptable to create dynamic mocks of this nature where a many-to-many relationship exists, these relationships are covered in the next section.
It is important to note that, at this point, we are beginning to work with references. An object can now exist in the default collection for its type as well as any number of derived collections attached to other objects. These types of dynamic mocks serve as an easy way to generate somewhat unique collections of sub-items rather than manually creating additional, uniquely mocked sets of data for each parent. Any direct property manipulation of items in these derived collections will be present in other collection items if these references are preserved. This behavior is always undesirable and can be avoided with the combination of a map with a deep or shallow copy.
const applications: Application = [
{
applicationRef: 1,
name: 'App 1',
description: '',
roles: pickRandomSubset(mockRoles, 5).map(deepCopy),
},
{
applicationRef: 2,
name: 'App 2',
description: '',
roles: [mockRoles[0], mockRoles[5], mockRoles[6]].map(r => {...r}),
},
]
While the choice to leave these references or eliminate them is optional, if you find that you want an update applied to an object in a derived collection to appear globally for that object then you are almost certainly trying to emulate some type of many-to-many relationship and should stop immediately and use a default collection for that object across the board instead.
Generally speaking, mocked sub-collections generated at runtime are nothing more than a convenience feature that allows us to create seemingly unique collections of objects that would normally be unique to a particular parent object. The default collection for a type in this use case should be thought of as more of a copy source than an actual collection to be used in future requests or in ways other than to conveniently set up other mocked data sets. Rather than dealing with the generation of random collections of objects which also have randomly generated property values or manually creating/copying and storing uniquely mocked objects for each parent we instead randomly pick and copy from a default collection for that type.
While the decision to take mocks this far does give us a little more, though not completely, realistic behavior in our UI when working with mocked handlers, it is completely optional and should be exercised with caution. It is perfectly acceptable to use a single master collection or manually create sub-collections of mocks in these use cases.
#
4. Avoid complex, hierarchical and many-to-many relationships
It's tempting to generate hierarchical structures of mocks that mirror the database and express relationships between mocked objects. While doing so would allow us to make run time updates and reflect changes in these relationships in a realistic way, we do not want to create a fully featured API that runs locally in the browser or testing environment.
Establishing these relationships in our mocks introduces the expectation of fully featured API behavior to testers, other developers, and stakeholders while introducing unnecessary strain, potential bugs, and increased maintenance costs as mocks and handlers are updated to stay in-step with changes to the API and data relationships over time.
In cases where a many-to-many relationship is applicable, create one set of mock data for each type and let all parents share the same set of children. It is perfectly ok for returns of mocked data to be nonsensical in regards to refs, IDs, or any other type of foreign key or many-to-many relationship. As previously stated, the goal is to get data on the screen and test UI behavior, not API functionality and relationships between data.
#
Example
Consider the following types:
type Application = {
applicationRef: number;
name: string;
description: string;
roles: Role[];
};
type Role = {
roleRef: number;
applicationRef: number; // FK to Application.applicationRef
name: string;
description: string;
actions: Action[];
};
In your mocks, it may be tempting to create roles whose applicationRef values correspond to a particular application in your mock data. Then, for a request such as GET /application/:applicationRef/role you can to manually filter your mocked roles to those which belong to that particular application.
This seems like a good idea at first - as you interact with the UI and browse through your app, all of the data makes sense and each application detail page you visit has a unique set of roles you can view and interact with. However, as more relationships are established and expressed across different types within the data, these handlers can quickly become difficult to reason about and maintain. Buggy, inconsistent behavior can also be introduced that exists in your mocks but not in the API. The added complexity and maintenance of these mocks is not worth the cost of returning sensible data in all cases.
We have found that selecting a single item belonging to a collection and/or selecting a collection of sub-items with a one-to-one relationship copied directly to a parent are acceptable and maintainable practices, but anything beyond that should be avoided entirely.
For the request GET /application/:applicationRef it is ok to find the single application within your mocked applications array or return a 404 if not found. Given that a user or test runner selected this application from a previous call to GET /application it is extremely unlikely that the application won't be found, but attempting to locate the application and returning it or responding with a 404 otherwise is acceptable.
For the request GET /application/:applicationRef/role, however, it is not acceptable to filter roles having an applicationRef corresponding to that of the URL parameter. It is also unacceptable to coerce or mutate the mocked roles into having an applicationRef that matches the request parameter. Simply return the same set of mocked roles for all applications or generate a derived collection at runtime as described earlier, you will spend much less time on the task and avoid all of the pitfalls laid out in this recommendation.
To consider how managing these hierarchies can get us into trouble as an application grows, let's add a couple more entities to the mix:
type User = {
userRef: number;
name: string;
email: string;
roles: Role[];
actions: Action[];
};
type Action = {
actionRef: number;
applicationRef: number; // FK to Application.applicationRef
name: string;
description: string;
};
For this data set, a user object exists globally but can also be linked to a single application if roles corresponding to that application are applied to it. Roles and actions are unique to an application and are linked by foreign key. Actions are linked to roles, roles are linked to users, and both of these relationships are managed via many-to-many tables in the database.
The following endpoints may be used to retrieve data in the context of these relationships (path arguments abbreviated):
The retrieval of data in the contexts of these relationships does not seem all that complex on the surface. We might consider that we could set up some mock data that has proper foreign key values, make few calls to .filter to get us most of the way there, then add a Set to handle the case of unique user actions within an application and we're done. This is a reasonable thought at first, but, ultimately, does not meet the requirement that a user object exists globally but can also appear in the context of a single application when that application's roles are applied to it. This same problem is presented in the context of a single application when applying actions to roles.
We could try and ignore the problem that we need many-to-many tables in order to manage these relationships by simply building larger sets of mock data that have unique users, roles, and actions per application. We could also solve the problem by building additional data structures and writing support code that handles them. While this is certainly possible, it quickly becomes time consuming and, ultimately, not worth the effort of building and maintaining the mock data, logic, and handlers required to do so. We can greatly reduce the complexity and overhead by simply returning the same users, roles, and actions for all relationships in all applications.
It is true that this approach loses the benefit of mocked data making sense in all cases. However, we can still render the entire UI with these simple mocks while covering the vast majority of use cases. Any other approach very quickly snowballs into building a complete API and database implementation which add unnecessary complexity, contribute to missed deadlines and wasted effort, and lead to buggy behaviors and incorrect results that distract users, testers and other developers.
#
5. Mutate mock data within reason
When mocking mutations, it is okay to apply modifications to mocks in-memory insofar as it is reasonable and limited to a single level of nesting. Mutations should be limited to and contained within a single type and should not cross boundaries into the relationships between other mocks.
#
Creating new items
For mutations that create a new item in a collection do not initialize fields to anything other than the default value for that type.
For example, do not set a field representing when the item was to today or anything other than empty string. If a field is nullable, let it be null unless provided in the request. If you are mocking data for a React application, be sure that the id or ref field is unique to avoid issues with duplicate key props in components (we have a findMaxOfProp helper for this).
http.post(`${BASE_URL}/v1/role`, async ({ request }) => {
const body = (await request.json()) as CreateRoleArgs;
const nextRef =
findMaxOfProp("roleRef", mockApplicationRolesResponse.results) + 1;
mockApplicationRolesResponse.results.push({
roleRef: nextRef,
createDate: "",
createUserRef: 0,
...body,
});
return HttpResponse.json(mockRoleResponse(nextRef), { status: 200 });
});
#
Updating existing items
When updating an existing item within a collection, if the item cannot be found a 404 should be returned. Once located, the request body can be spread into the existing item, re-inserted at the correct index, and returned.
http.patch(`${BASE_URL}/v1/role/:roleRef`, async ({ request }) => {
const body = (await request.json()) as UpdateRoleArgs;
const idx = mockRolesResponse.results.findIndex(
(r) => r.roleRef === body.roleRef
);
if (idx < 0) {
return HttpResponse.json(null, { status: 404 });
}
const updatedRole = {
...mockRolesResponse.results[idx],
...body,
};
mockRolesResponse.results.splice(idx, 1, updatedRole);
return HttpResponse.json(updatedRole, { status: 200 });
});
#
Deleting items
It is acceptable to delete items from a single collection by locating the index of the item and removing it, a 404 response should be returned if not found.
http.delete(`${BASE_URL}/v1/role/:roleRef`, async ({ params }) => {
const roleRef = Number(params.roleRef);
const idx = mockRolesResponse.results.findIndex((r) => r.roleRef === roleRef);
if (idx < 0) {
return HttpResponse.json(null, { status: 404 });
}
mockRolesResponse.results.splice(idx, 1);
return HttpResponse.json(null, { status: 200 });
});
It is not acceptable, however, to delete items from a many-to-many relationship. For example, the request DELETE /v1/role/:roleRef/action/:actionRef would affect the many-to-many relationship of actions to roles. This request should be handled by simply returning a 200 and moving on.
#
General guidance
If you find yourself tempted to handle state changing requests for a URL with a path beyond a single item in a root-level collection, stop and simply return a 200 response.
Remember that because we only maintain one set of data for each type (sometimes multiple sub-types), that mutation will be universal for all parent entities and collections.