」工欲善其事,必先利其器。「—孔子《論語.錄靈公》
首頁 > 程式設計 > 模擬網路請求變得容易:整合 Jest 和 MSW

模擬網路請求變得容易:整合 Jest 和 MSW

發佈於2024-11-02
瀏覽:345

Writing unit tests that involve mocking or stubbing API calls can feel overwhelming—I’ve been there myself. In this article, I’ll guide you through a simpler, more efficient way to mock API requests during testing.
Before we dive in, here’s a list of tools we’ll be using:

  • React
  • React Testing Library
  • Jest
  • Craco : > we will need this to extend jest config later on
  • MSW (mock service worker)

Don’t worry if you’re not familiar with everything—just follow along, and I’ll walk you through each step.

Here is the Github link to the project

Overview
We’ll be building a simple React app that fetches and displays a list of Todos using the Json Placeholder API. In this project, we’ll write test scenarios to verify the following:

  • The app shows a loading state while the API request is in progress.
  • It displays an error message if the request fails.
  • It ensures the list of data from the API is correctly rendered.
  • It provides feedback when the request is successful but returns no data.

This guide will cover two main approaches;

  1. The Traditional Way of Mocking API Calls
  2. The Modern Approach Using MSW (Mock Service Worker)

Let's get started!

To start building the app, follow these steps:

1. Create a new React app: Run the following command to create your React application:

npx create-react-app msw-example

2. Start the application: After the setup, navigate to the project folder and run:

npm start

3. Install necessary packages: Next, install @tanstack/react-query to manage client-side data:

npm install @tanstack/react-query

React Query (Tanstack Query) helps in handling server-side state management, including caching, synchronization, and data fetching. You can learn more about it in their Official Documentation

You can now start writing the app logic and set up React Query to manage data fetching efficiently. Here is what it looks like after setting it up.

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

import { Todos } from "components/Todos";

const queryClient = new QueryClient();

function App() {
  return (
    
      
); } export default App;

Next we create the Todos component rendering a list of our todos.

Todos.js

import { useQuery } from "@tanstack/react-query";

import { getTodos } from "api/todo";

export function Todos() {
  const { data, isError, isLoading } = useQuery({
    queryFn: getTodos,
    queryKey: ["TODOS"],
  });

  if (isLoading) {
    return 

loading todo list

; } if (isError) { return

an error occurred fetching todo list

; } return (
{Boolean(data.length) ? (
    {data.map((item) => (
  1. {item.title}
  2. ))}
) : (

You do not have todos created yet

)}
); }

Now that everything is set up, let’s start writing the tests for the scenarios we outlined earlier. First, we’ll implement this using the traditional approach that we’re already familiar with—mocking API calls using Jest

Checkout the github repo so you can follow along

The Traditional Way of Mocking API Calls

React and Jest work seamlessly together out of the box, so there’s no need for additional configuration or setup—at least for now. We’ll create a file named Todos.test.js next to our Todo.js component, where we’ll import the Todos component and write our tests..

We have a function called getTodos which is responsible for making an API call to retrieve the list of todos and returning the response.

export async function getTodos() {
  const response = await fetch("https://jsonplaceholder.typicode.com/todos", {
    headers: {
      "Content-Type": "application/json",
    },
  });

  if (!response.ok) {
    const res = await response.json();
    throw new Error(res.message || response.status);
  }

  return response.json();
}


In your Todos.test.js file, you need to import the Todos component and create a utility function that provides a wrapper with the React Query provider. This ensures that the Todos component and its children can use react-query for managing server state in tests.

import { render, screen, waitFor, within } from "@testing-library/react";

import { Todos } from "./Todos";
import { reactQueryWrapper } from "utils/reactQueryWrapper";
import { getTodos } from "api/todo";

const { wrapper, queryCache } = reactQueryWrapper();

Next, we need to mock the getTodos function. This will allow us to specify the return values for each test scenario, giving us control over the data the function returns during testing. Additionally, we’ll ensure that any leftover data from previous test cases is cleaned up, so each test case starts with a clean slate.

Code Sample

jest.mock("api/todo", () => ({
  getTodos: jest.fn(),
}));

afterEach(() => {
  queryCache.clear();
  jest.clearAllMocks();
});

Todos.test.js

import { render, screen, waitFor, within } from "@testing-library/react";

import { Todos } from "./Todos";
import { reactQueryWrapper } from "utils/reactQueryWrapper";
import { getTodos } from "api/todo";

const { wrapper, queryCache } = reactQueryWrapper();

jest.mock("api/todo", () => ({
  getTodos: jest.fn(),
}));

afterEach(() => {
  queryCache.clear();
});

afterEach(() => {
  jest.clearAllMocks();
});

First Test Scenario: Renders loading state
We want to verify that our component correctly displays the loading state while the request is in progress.

test("Renders loading state", () => {
  getTodos.mockImplementation(() => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve();
      }, 1000);
    });
  });

  render(, { wrapper });
  const loadingText = screen.getByText("loading todo list");
  expect(loadingText).toBeInTheDocument();
});

Second Test Scenario: Renders error state when request fails or there is network error
We want to verify that the component correctly renders an error state when the API request fails or encounters a network error.

test("Renders error state when request fails or there is network error", async () => {
  getTodos.mockImplementationOnce(() => {
    return new Promise((resolve, reject) => {
      reject();
    });
  });

  render(, { wrapper });
  await screen.findByText("an error occurred fetching todo list");
  expect(
    screen.getByText("an error occurred fetching todo list")
  ).toBeInTheDocument();
});

Third Test Scenario: Renders list of todos
We want to verify that our component correctly renders the list of todos when the request is successful.

test("Renders list of todos", async () => {
  getTodos.mockImplementation(() => {
    return Promise.resolve([
      { id: 1, title: "Exercise" },
      { id: 2, title: "Cook" },
    ]);
  });

  render(, { wrapper });

  const loadingText = screen.queryByText("loading todo list");
  await waitFor(() => expect(loadingText).not.toBeInTheDocument());
  const list = screen.getByRole("list");
  expect(list).toBeInTheDocument();
  expect(within(list).getAllByRole("listitem")).toHaveLength(2);
});

Fourth Test Scenario: Renders list of todos
We want to verify that your component correctly renders a feedback message when the API request returns an empty list of todos.

test("Renders feedback message when user has an empty list of todos", async () => {
  getTodos.mockImplementationOnce(() => {
    return Promise.resolve([]);
  });

  render(, { wrapper });

  await waitFor(() =>
    expect(screen.queryByText("loading todo list")).not.toBeInTheDocument()
  );
  expect(
    screen.getByText("You do not have todos created yet")
  ).toBeInTheDocument();
});

Great! Now that we've covered mocking API calls with Jest, let’s explore a better approach using Mock Service Worker (MSW). MSW provides a more elegant and maintainable way to mock API calls by intercepting network requests at the network level rather than within your tests.

Introducing MSW (Mock Service Worker)

Mock Service Worker (MSW) is an API mocking library designed for both browser and Node.js environments. It allows you to intercept outgoing requests, observe them, and provide mocked responses. MSW helps you simulate real-world scenarios in your tests, making them more robust and reliable.

Read more about MSW

Setting Up MSW

Step 1: Install MSW using the following command:

npm install msw@latest --save-dev

Step 2: Set up the environment you wish to intercept requests in—either Browser or Node. Before doing so, create a mock directory within your src directory. Inside this directory, you'll create the following files and directories:

browser.js: Handles request interception in the browser environment.
server.js: Handles request interception in the Node.js environment.
handlers: A directory containing files that define the API endpoints to intercept.

Here’s how your folder structure should look:

src/
  └── mock/
      ├── browser.js
      ├── server.js
      └── handlers/

This setup ensures that you have a clear organization for intercepting and handling requests in both browser and Node.js environments using MSW.

Browser Environment Setup

To set up MSW for intercepting requests in the browser, follow these steps:

1. Create the browser.js File

In your src/mock directory, create a file named browser.js. This
file will set up the MSW worker to intercept requests in the
browser environment.

// src/mock/browser.js

import { setupWorker } from 'msw/browser';

// Create a worker instance to intercept requests
export const worker = setupWorker();

2. Generate the mockServiceWorker.js File
This file is required for MSW to function properly in the browser.
Generate it using the following command from the root directory of
your application:

npx msw init  --save

This command initializes the MSW service worker and places the mockServiceWorker.js file into the public directory of your React app.

3. Start the Service Worker

Import and start the worker in your application entry point
(typically index.js or App.js).

// src/index.js

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";

if (process.env.NODE_ENV === "development") {
  const mswState = localStorage.getItem("mswState") === "enabled";
  if (mswState) {
    const { worker } = require("./mocks/browser");
    worker.start();
    window.__mswStop = worker.stop;
  }
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  
    
  
);

4. Verify the Setup
To ensure that the service worker is correctly set up, navigate to
the URL of your application in the browser:

http://localhost:3000/mockServiceWorker.js

You should see the service worker script displayed in your browser. This confirms that the service worker is correctly installed and ready to intercept requests.

If your MSW setup is correct and enabled, you should see a console message indicating that MSW is active. Your browser console should display logs similar to this:

Mocking Network Requests Made Easy: Integrating Jest and MSW

These logs confirm that the MSW service worker is properly intercepting network requests and is ready to mock API responses according to the handlers you’ve defined.

Node Environment Setup

To set up MSW for intercepting requests in a Node.js environment (for example, in server-side tests), follow these steps:

Step 1: Create the server.js File
In the src/mock directory, create a file named server.js. This file sets up the MSW server to intercept requests in a Node.js environment.

// src/mock/server.js

import { setupServer } from "msw/browser";

// Create a server instance with the defined request handlers
export const server = setupServer();

Step 2: Define the API Handlers
Create a file named posts.js in the handlers directory.This file will describe the APIs you want to intercept and the mock responses.

// src/mock/handlers/posts.js

import { http, HttpResponse } from "msw";

export const postHandlers = [

 // Handler for GET /todos request
  http.get("https://jsonplaceholder.typicode.com/todos", () => {
    return HttpResponse.json([
      { id: 1, title: "totam quia non" },
      { id: 2, title: "sunt cum tempora" },
    ]);
  }),
];

Here, we're defining that when MSW intercepts a GET request to https://jsonplaceholder.typicode.com/todos, it should respond with a 200 status code and the provided JSON data.

Step 3: Hook Handlers to the Browser Worker
Update the browser.js file to include the defined handlers.

import { setupWorker } from "msw/browser";

import { postHandlers } from "./handlers/posts";

export const worker = setupWorker(...postHandlers);

Step 4: Hook Handlers to the Node Server
Ensure the handlers are also used in the Node.js environment by updating the server.js file.

import { setupServer } from "msw/node";

import { postHandlers } from "./handlers/posts";

export const server = setupServer(...postHandlers);

With these configurations in place, your MSW setup is complete and ready for both browser and Node.js environments. Congratulations on completing the setup! ?

Using MSW in our Tests
To use MSW in your tests, you need to set up your test environment to utilize the mock server for intercepting API calls. Here’s a guide to setting up and writing tests using MSW with your Todos component.

  1. Create the Test File
    Create a new file named Todos.MSW.test.js next to your
    Todos.jscomponent. This file will contain your tests that
    utilize MSW for mocking API responses.

  2. Set Up Test Environment
    In your Todos.MSW.test.js file, import the necessary modules and
    set up the environment for using MSW with your tests. Below is an
    example setup:

import { render, screen, waitFor, within } from "@testing-library/react";
import { http, delay, HttpResponse } from "msw";

import { Todos } from "./Todos";
import { reactQueryWrapper } from "utils/reactQueryWrapper";
import { server } from "mocks/server";

const { wrapper, queryCache } = reactQueryWrapper();

afterEach(() => {
  queryCache.clear();
});

afterEach(() => {
  jest.clearAllMocks();
});

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

First Test Scenario: Renders loading state
We want to verify that our component correctly displays the loading state while the request is in progress.

test("Renders loading state", () => {
  server.use(
    http.get("https://jsonplaceholder.typicode.com/todos", async () => {
      await delay(1000);
      return HttpResponse.json([]);
    })
  );

  render(, { wrapper });
  const loadingText = screen.getByText("loading todo list");
  expect(loadingText).toBeInTheDocument();
});

Second Test Scenario: Renders error state when request fails or there is network error
We want to verify that the component correctly renders an error state when the API request fails or encounters a network error.

test("Renders error state when request fails or there is network error", async () => {
  server.use(
    http.get("https://jsonplaceholder.typicode.com/todos", async () => {
      return HttpResponse.json([], {
        status: 500,
      });
    })
  );

  render(, { wrapper });
  await screen.findByText("an error occurred fetching todo list");
  expect(
    screen.getByText("an error occurred fetching todo list")
  ).toBeInTheDocument();
});

Third Test Scenario: Renders list of todos
We want to verify that our component correctly renders the list of todos when the request is successful.

test("Renders list of todos", async () => {

  render(, { wrapper });

  const loadingText = screen.queryByText("loading todo list");
  await waitFor(() => expect(loadingText).not.toBeInTheDocument());
  const list = screen.getByRole("list");
  expect(list).toBeInTheDocument();
  expect(within(list).getAllByRole("listitem")).toHaveLength(2);
});

Fourth Test Scenario: Renders list of todos
We want to verify that your component correctly renders a feedback message when the API request returns an empty list of todos.

test("Renders feedback message when user has an empty list of todos", async () => {
   server.use(
    http.get("https://jsonplaceholder.typicode.com/todos", () => {
      return HttpResponse.json([]);
    })
  );
  render(, { wrapper });

  await waitFor(() =>
    expect(screen.queryByText("loading todo list")).not.toBeInTheDocument()
  );
  expect(
    screen.getByText("You do not have todos created yet")
  ).toBeInTheDocument();
});

Final Thoughts
Mocking API calls is crucial for effective testing, allowing you to simulate different scenarios without relying on real backend services. While traditional Jest mocking can be effective, MSW offers a more sophisticated solution with better support for various environments and more realistic request handling.

Here are a few tips to keep in mind:

  • Choose the Right Tool: Use MSW for a more comprehensive
    solution that integrates seamlessly with your React application,
    especially when dealing with complex request handling.

  • Organize Your Handlers: Keep your API handlers well-organized
    and modular to make them easier to maintain and extend.

  • Clean Up After Tests: Ensure that your tests clean up properly
    by resetting handlers and clearing mocks to avoid interference
    between tests.

  • Verify Setup: Always check your setup by inspecting the network
    requests and responses to ensure that everything is working as
    expected.
    By incorporating MSW into your testing strategy, you'll achieve more reliable and maintainable tests, leading to a smoother development experience and higher quality software.

Happy testing! ?

版本聲明 本文轉載於:https://dev.to/rolandawoda/mocking-network-requests-made-easy-integrating-jest-and-msw-58ak?1如有侵犯,請聯絡[email protected]刪除
最新教學 更多>
  • 版本5.6.5之前,使用current_timestamp與時間戳列的current_timestamp與時間戳列有什麼限制?
    版本5.6.5之前,使用current_timestamp與時間戳列的current_timestamp與時間戳列有什麼限制?
    在時間戳列上使用current_timestamp或MySQL版本中的current_timestamp或在5.6.5 此限制源於遺留實現的關注,這些限制需要對當前的_timestamp功能進行特定的實現。 創建表`foo`( `Productid` int(10)unsigned not ...
    程式設計 發佈於2025-07-18
  • 對象擬合:IE和Edge中的封面失敗,如何修復?
    對象擬合:IE和Edge中的封面失敗,如何修復?
    To resolve this issue, we employ a clever CSS solution that solves the problem:position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%)...
    程式設計 發佈於2025-07-18
  • 如何在鼠標單擊時編程選擇DIV中的所有文本?
    如何在鼠標單擊時編程選擇DIV中的所有文本?
    在鼠標上選擇div文本單擊帶有文本內容,用戶如何使用單個鼠標單擊單擊div中的整個文本?這允許用戶輕鬆拖放所選的文本或直接複製它。 在單個鼠標上單擊的div元素中選擇文本,您可以使用以下Javascript函數: function selecttext(canduterid){ if(d...
    程式設計 發佈於2025-07-18
  • MySQL中如何高效地根據兩個條件INSERT或UPDATE行?
    MySQL中如何高效地根據兩個條件INSERT或UPDATE行?
    在兩個條件下插入或更新或更新 solution:的答案在於mysql的插入中...在重複鍵更新語法上。如果不存在匹配行或更新現有行,則此功能強大的功能可以通過插入新行來進行有效的數據操作。如果違反了唯一的密鑰約束。 實現所需的行為,該表必須具有唯一的鍵定義(在這種情況下為'名稱'...
    程式設計 發佈於2025-07-18
  • 用戶本地時間格式及時區偏移顯示指南
    用戶本地時間格式及時區偏移顯示指南
    在用戶的語言環境格式中顯示日期/時間,並使用時間偏移在向最終用戶展示日期和時間時,以其localzone and格式顯示它們至關重要。這確保了不同地理位置的清晰度和無縫用戶體驗。以下是使用JavaScript實現此目的的方法。 方法:推薦方法是處理客戶端的Javascript中的日期/時間格式化和...
    程式設計 發佈於2025-07-18
  • 如何檢查對像是否具有Python中的特定屬性?
    如何檢查對像是否具有Python中的特定屬性?
    方法來確定對象屬性存在尋求一種方法來驗證對像中特定屬性的存在。考慮以下示例,其中嘗試訪問不確定屬性會引起錯誤: >>> a = someClass() >>> A.property Trackback(最近的最新電話): 文件“ ”,第1行, AttributeError: SomeClass...
    程式設計 發佈於2025-07-18
  • 為什麼不使用CSS`content'屬性顯示圖像?
    為什麼不使用CSS`content'屬性顯示圖像?
    在Firefox extemers屬性為某些圖像很大,&& && && &&華倍華倍[華氏華倍華氏度]很少見,卻是某些瀏覽屬性很少,尤其是特定於Firefox的某些瀏覽器未能顯示圖像時未能顯示圖像時遇到了一個問題。這可以在提供的CSS類中看到:。 googlepic { 內容:url(&...
    程式設計 發佈於2025-07-18
  • 如何從Google API中檢索最新的jQuery庫?
    如何從Google API中檢索最新的jQuery庫?
    從Google APIS 問題中提供的jQuery URL是版本1.2.6。對於檢索最新版本,以前有一種使用特定版本編號的替代方法,它是使用以下語法:獲取最新版本:未壓縮)While these legacy URLs still remain in use, it is recommended ...
    程式設計 發佈於2025-07-18
  • 左連接為何在右表WHERE子句過濾時像內連接?
    左連接為何在右表WHERE子句過濾時像內連接?
    左JOIN CONUNDRUM:WITCHING小時在數據庫Wizard的領域中變成內在的加入很有趣,當將c.foobar條件放置在上面的Where子句中時,據說左聯接似乎會轉換為內部連接。僅當滿足A.Foo和C.Foobar標準時,才會返回結果。 為什麼要變形?關鍵在於其中的子句。當左聯接的右側...
    程式設計 發佈於2025-07-18
  • 我可以將加密從McRypt遷移到OpenSSL,並使用OpenSSL遷移MCRYPT加密數據?
    我可以將加密從McRypt遷移到OpenSSL,並使用OpenSSL遷移MCRYPT加密數據?
    將我的加密庫從mcrypt升級到openssl 問題:是否可以將我的加密庫從McRypt升級到OpenSSL?如果是這樣,如何? 答案:是的,可以將您的Encryption庫從McRypt升級到OpenSSL。 可以使用openssl。 附加說明: [openssl_decrypt()函數要求...
    程式設計 發佈於2025-07-18
  • 為什麼Microsoft Visual C ++無法正確實現兩台模板的實例?
    為什麼Microsoft Visual C ++無法正確實現兩台模板的實例?
    The Mystery of "Broken" Two-Phase Template Instantiation in Microsoft Visual C Problem Statement:Users commonly express concerns that Micro...
    程式設計 發佈於2025-07-18
  • 在Python中如何創建動態變量?
    在Python中如何創建動態變量?
    在Python 中,動態創建變量的功能可以是一種強大的工具,尤其是在使用複雜的數據結構或算法時,Dynamic Variable Creation的動態變量創建。 Python提供了幾種創造性的方法來實現這一目標。 利用dictionaries 一種有效的方法是利用字典。字典允許您動態創建密鑰並...
    程式設計 發佈於2025-07-18
  • 如何限制動態大小的父元素中元素的滾動範圍?
    如何限制動態大小的父元素中元素的滾動範圍?
    在交互式接口中實現垂直滾動元素的CSS高度限制問題:考慮一個佈局,其中我們具有與用戶垂直滾動一起移動的可滾動地圖div,同時與固定的固定sidebar保持一致。但是,地圖的滾動無限期擴展,超過了視口的高度,阻止用戶訪問頁面頁腳。 $("#map").css({ margin...
    程式設計 發佈於2025-07-18
  • 解決Spring Security 4.1及以上版本CORS問題指南
    解決Spring Security 4.1及以上版本CORS問題指南
    彈簧安全性cors filter:故障排除常見問題 在將Spring Security集成到現有項目中時,您可能會遇到與CORS相關的錯誤,如果像“訪問Control-allo-allow-Origin”之類的標頭,則無法設置在響應中。為了解決此問題,您可以實現自定義過濾器,例如代碼段中的MyFi...
    程式設計 發佈於2025-07-18
  • 如何正確使用與PDO參數的查詢一樣?
    如何正確使用與PDO參數的查詢一樣?
    在pdo 中使用類似QUERIES在PDO中的Queries時,您可能會遇到類似疑問中描述的問題:此查詢也可能不會返回結果,即使$ var1和$ var2包含有效的搜索詞。錯誤在於不正確包含%符號。 通過將變量包含在$ params數組中的%符號中,您確保將%字符正確替換到查詢中。沒有此修改,PD...
    程式設計 發佈於2025-07-18

免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。

Copyright© 2022 湘ICP备2022001581号-3