Creating mocks for testing react code
Published: 2024-04-21 22:16:24

When dealing with components that fetch data from APIs, writing tests can become really hard due to external dependencies. This is where mocks and stubs come in, providing a controlled environment to isolate and test your component's logic. In this post we will explore what mocks and stubs are, how they differ, and how Mock Service Worker (MSW) simplifies API mocking in Next.js project using Jest and React Testing Library.

## Mocks vs Stubs: What’s the Difference?
In testing, both mocks and stubs are types of test doubles - objects that simulate the behavior of real objects. They are used to isolate the code under test from the rest of the system.

- **Mock**: Replicate the entire behavior of an object, including side effects and interactions with other components. Mocks are often used for complex dependencies with intricate behavior. A mock ia an object that pre-programmed with expectations which form a specification of the calls they are expected to receive.

- **Stub**: Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test. They typically focus on returning predefined values or performing specific actions without replicating the complete behavior. 

Roy Osherove in his book **The art of Unit Testing**

> The easiest way to tell we’re dealing with a stub is to notice that the stub can never fail the test. The asserts the test uses are always against the class under test.

> On the other hand, the test will use a mock object to verify whether the test failed or not. [...]

> Again, the mock object is the object we use to see if the test failed or not.

## Create Stubs and Mocks with jest
For example, I have `UbahJadwal` component that depend on a custom `useJadwalKonselor` hooks for displaying some data. This component also use `useSearchParams` method from next/navigation and a method to displaying toast.


![image 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bq640ial4gm4nwnne1gk.png)

Since we use many external dependencies in the component, all tests will surely fail if we don't create test doubles for all the dependencies.

![image1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hpjuqdn68t0qvhxkeydo.png)

From the error description, we can see why it fails because of the `useSearchParam().get()` didn't return anything. To solve this issue we can simply create a **`stub`** for the get function.


```typescript
jest.mock("next/navigation", () => {
  return{
    useSearchParams: jest.fn(() => ({
      get: jest.fn().mockResolvedValue(""),
    }
    ))
  }
})
```

---

Now, some of the tests from the test suite have already passed.
![Images](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0w649fdcxnzglhtsz4nf.png)
The next problem is the component should be populated by data from `useJadwalKonselor` hooks. What The hooks do is simply fetch data to be displayed inside a Modal. We can mock the implementation of the hooks to return the API response. 

```typescript
jest.mock("../../../../../src/hooks/bkm-1/useJadwalKonselor.ts", () => ({
  useJadwalKonselor: jest.fn().mockImplementation(() => {
    return {
      data: {
        id: "59f30d61-4cc6-4ec3-90c1-c11663217889",
        nama: "Konselor B",
        jadwal_praktik: [
          {
            nama_hari: "Selasa",
            waktu_mulai: "10:00:00",
            waktu_selesai: "16:00:00",
          },
          {
            nama_hari: "Kamis",
            waktu_mulai: "12:00:00",
            waktu_selesai: "15:00:00",
          },
        ],
      },
      isLoading: false,
      error: null,
    }})
}))
```
We can see the all tests passed now

![result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qxh1qw5e1r59read33nq.png)



## Mocking API calls
Let's consider we want to mock an API call to fetch some data, what we can do is we mock the global fetch function like this.
```typescript
    global.fetch = jest.fn(
      () =>
        Promise.resolve({
          json: () =>
            Promise.resolve({ data: [{
          "nama_pasien": "A",
          "is_pagi": true,
          "is_terjadwal": true,
          "skala_prioritas": 10,
          "created_at": "2024-01-01 00:00:00+0000",
          "id_pendaftaran": "00000000-0000-0000-0000-000000000000",
        }]}),
          ok: true,
        }) as Promise<Response>
    )
```
The above code will do the job. The problems come when we have multiple fetch functions inside one component (fetch with GET and POST/PUT methods).

## Mocking with MSW
While mocks are effective, they require modifying the component's internal logic or mocking global functions like fetch. This can become cumbersome for complex components with numerous API interactions. Here's where [MSW](https://mswjs.io/) shines.

MSW is a library that lets you create a mock service worker that intercepts all network requests made from your frontend application. It allows you to define handlers that specify how to respond to specific API calls with predefined data or errors. This approach offers several benefits:

- **Isolation**: Isolates your frontend from external dependencies for reliable and consistent tests.
- **Flexibility**: Easily define various responses for different scenarios without modifying component logic.
- **Simplicity**: Provides a cleaner testing experience compared to manual mocks and stubs.
- **Network Interception**: Captures actual network requests for inspection and debugging.

Here’s a basic example of how to use MSW to mock a GET request:
```typescript
import { rest } from "msw"
import { setupServer } from "msw/node"
const handlers = [
  rest.get(
    process.env.NEXT_PUBLIC_API_URL_1 +
      "/api/konseling/konselor/59f30d61-4cc6-4ec3-90c1-c11663217889/",
    (req,res,ctx) => {
      return res(ctx.json({
        "id": "59f30d61-4cc6-4ec3-90c1-c11663217889",
        "nama": "Konselor B",
        "no_wa": "0857651920018",
        "jadwal_praktik": [
          {
            "urutan_hari": 1,
            "nama_hari": "Selasa",
            "waktu_mulai": "10:00:00",
            "waktu_selesai": "16:00:00"
          },
          {
            "urutan_hari": 3,
            "nama_hari": "Kamis",
            "waktu_mulai": "12:00:00",
            "waktu_selesai": "15:00:00"
          }
        ],
        "tipe": "Konselor"
      }),ctx.status(200))
    }
  ),
  rest.get(process.env.NEXT_PUBLIC_API_URL_1 +
      "/api/konseling/konselor/2d142202-acbf-4cfd-8ba7-8e02d7857056/",
  (req,res,ctx) => {
    return res(ctx.json({
      "error": "Konselor tidak ditemukan"}
    ),ctx.status(404))}),

  rest.get(process.env.NEXT_PUBLIC_API_URL_1 +
      "/api/konseling/konselor/39a3ee55-8bcb-4bc8-9212-875c92ff4311/",
  (req,res,ctx) => {
    return res(ctx.json({
      "INTERNAL_SERVER_ERROR": "Internal Server Error"}
    ),ctx.status(500))}),
]

```

```typescript

const server = setupServer(...handlers)

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

describe("useDetailKonselor", () => {
  it("should return detail konselor", async () => {
    const { result } = renderHook(() => useDetailKonselor("59f30d61-4cc6-4ec3-90c1-c11663217889"))
    
    expect(result.current.isLoading).toBeTruthy()

    await waitFor(() => expect(result.current.isLoading).toBeFalsy())
    expect(result.current.data).toEqual({
      "id": "59f30d61-4cc6-4ec3-90c1-c11663217889",
      "nama": "Konselor B",
      "no_wa": "0857651920018",
      "jadwal_praktik": [
        {
          "urutan_hari": 1,
          "nama_hari": "Selasa",
          "waktu_mulai": "10:00:00",
          "waktu_selesai": "16:00:00"
        },
        {
          "urutan_hari": 3,
          "nama_hari": "Kamis",
          "waktu_mulai": "12:00:00",
          "waktu_selesai": "15:00:00"
        }
      ],
      "tipe": "Konselor"
    })
  })

  it("should return error when konselor not found", async () => {
    const { result } = renderHook(() => useDetailKonselor("2d142202-acbf-4cfd-8ba7-8e02d7857056"))
    await waitFor(() => expect(result.current.error).toBeDefined())
    
    expect(result.current.error?.message).toEqual("Konselor tidak ditemukan")
  })
})
```
The Result :

![result msw](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fakhot7kfxvnuugqbog6.png)