DEV Community

fajarriv
fajarriv

Posted on • Updated on

Creating mocks for testing react code

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

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

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.

jest.mock("next/navigation", () => {
  return{
    useSearchParams: jest.fn(() => ({
      get: jest.fn().mockResolvedValue(""),
    }
    ))
  }
})
Enter fullscreen mode Exit fullscreen mode

Now, some of the tests from the test suite have already passed.
Images
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.

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,
    }})
}))
Enter fullscreen mode Exit fullscreen mode

We can see the all tests passed now

result

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.

    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>
    )
Enter fullscreen mode Exit fullscreen mode

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 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:

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))}),
]

Enter fullscreen mode Exit fullscreen mode

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")
  })
})
Enter fullscreen mode Exit fullscreen mode

The Result :

result msw

Top comments (0)