Rapid API Mocking for Development

In the fast-paced world of development, there are instances when time is of the essence, and the luxury of installing robust libraries like msw or faker is not feasible. Whether due to stringent company policies requiring external security verification for all installed libraries or simply the urgency to get things done quickly, there's a need for a swift and uncomplicated solution. This is where a straightforward utility function, conveniently named mock, can prove invaluable, particularly for creating mock requests to backend endpoints for testing UI behavior. Let's delve into a discussion and implementation of this utility to address such scenarios.

Problem Scenario

Picture this: Your manager approaches you with a task—implement a view that requires calling multiple API endpoints. You're armed with the knowledge of the expected payloads and responses, but there's a hiccup. The backend developers are taking their time, and the APIs are not ready. To add to the challenge, your company has a default policy of blocking the installation of third-party tools until they pass an audit. It's a bit of a conundrum, but the show must go on, and you need to find a way to move forward with your work.

Solution Design

Let's outline the objectives for our mock function design to ensure it meets the required specifications:

Payload and Response Flexibility: We aim to allow users to specify the expected payload and define the desired response contract for each mock endpoint.

Delay Customization: It's crucial to provide developers with the ability to customize the delay of the response. This flexibility accommodates different testing scenarios and ensures realistic simulations.

Behavioral Customization: We want to introduce options for customizing the behavior of the mock function. Users should be able to decide whether to explicitly pass an error or randomly generate errors based on a specified factor.

By focusing on these design goals, we can create a versatile mock function that caters to various testing needs and aligns with the intended use cases.

interface GetUserPayload {
  id: number;
}

interface GetUserResponse {
  id: number;
  name: string;
}

// Response will be available after 2 second delay.
// 10% of requests will fail.
// The failure object will have shape of Error("ups").
const getUser = mock({
  delay: 2,
  errorFactor: 10,
  error: () => Error("ups"),
})<GetUserResponse>({
  id: 1,
  name: "test",
});

// Requires { id: 1 } object as a payload.
await getUser<GetUserPayload>({ id: 1 }); // Returns { id: 1, name: "test" } object.

Great! Now, let's discuss the practicality of using our mock function in a real-world scenario. The beauty lies in its seamless integration – in an actual use case, all you need to do is replace the import path. This simple action ensures that your mock function seamlessly transitions to working with the real endpoint, making it an effortlessly adaptable solution. This flexibility not only simplifies the testing process but also underscores the versatility of our mock function in real-world implementations.

// from 💥
import { getPosts } from "@mocks";
// to 💚
import { getPosts } from "@services";

Implementation

interface MockConfig {
  delay?: number; // Delay specified in seconds.
  errorFactor?: number; // % of requests will throw an error.
  error?(): unknown; // Function to generate error object.
}
// Generates a random integer between 0 and 100.
const getRandomNumber = (): number => Math.floor(Math.random() * 101);

const mock =
  ({ delay = 1, errorFactor = 0, error }: MockConfig = {}) =>
  <Response>(response: Response) =>
  <Payload>(payload: Payload): Promise<Response> => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (getRandomNumber() <= errorFactor) {
          reject(error?.() ?? Error(`Ups problem...`));
        }

        resolve(response);
      }, delay * 1000);
    });
  };

Usage Examples

const getPost = mock()({ id: 1, name: "test" });
const getUser = mock({ delay: 2 })({ id: 1, name: "test" });

console.log(await getPost({ id: 1 })); // { id: 1, name: "test" } after 1 second
console.log(await getUser({ id: 1 })); // { id: 1, name: "test" } after 2 seconds

Using Composition

interface GetUserPayload {
  id: number;
}

interface GetUserResponse {
  id: number;
  name: string;
}

const config = mock({
  delay: 2,
  errorFactor: 10,
  error: () => Error("ups"),
});

const getUser = config<GetUserResponse>({
  id: 1,
  name: "test",
});

const getUsers = config<GetUserResponse[]>([{ id: 1, name: "test" }]);

await getUser<GetUserPayload>({ id: 1 });
await getUser<GetUserPayload[]>([{ id: 1 }]);

Summary

As evident from the example, achieving a simple result like being able to proceed with your work daily or writing unit/integration tests doesn't necessitate the use of elaborate mocking libraries. Instead, a straightforward utility function can serve the purpose effectively. This emphasizes the power of simplicity, demonstrating that pragmatic solutions can often be more than sufficient for everyday development needs.