為React組件庫引入自動化測試:從零到完善的實踐之路
為什么我們需要測試?
我們的 React+TypeScript 業務組件庫已經穩定運行了一段時間,主要承載各類UI展示組件,如卡片、通知等。項目初期,迫于緊張的開發周期,我們暫時擱置了自動化測試的引入。當時團隊成員對組件邏輯了如指掌,即便沒有測試也能游刃有余。
然而隨著時間推移,問題逐漸顯現。當新成員加入或老組件需要迭代時,我們常常陷入兩難:修改代碼可能破壞原有功能,但不修改又無法滿足新需求。特別是在處理那些具有多種交互狀態的復雜組件時,手動測試變得既耗時又不可靠。這時,引入自動化測試的必要性就凸顯出來了。
搭建測試環境
依賴安裝
我們首先從安裝核心測試依賴開始,這些工具將構成我們測試體系的基礎框架:
- 測試運行核心:jest和jsdom環境包
- TypeScript支持:確保類型安全的測試環境
- React測試工具:專門為React組件設計的測試工具鏈
npm install jest jest-environment-jsdom @types/jest ts-jest @testing-library/react @testing-library/jest-dom @testing-library/user-event --save-dev
配置Jest
創建jest.config.ts配置文件時,有幾個關注點:
- 針對TypeScript項目的特殊處理
- 瀏覽器環境的模擬
- 測試初始化流程
- 文件轉換規則
module.exports = {
preset: "ts-jest", // 為 TypeScript 項目準備的 Jest 配置預設
testEnvironment: "jsdom", // 測試運行在模擬的瀏覽器環境中
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"], // 指定在測試環境初始化后立即執行的文件
transform: {
"^.+\\.(ts|tsx)$": "ts-jest", // 使用 ts-jest 處理所有 .ts 和 .tsx 文件
},
testPathIgnorePatterns: ["/node_modules/", "/dist/"], // 忽略指定目錄下的測試文件
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], // 定義 Jest 能識別的模塊文件擴展名
};
export {}; // 使文件成為模塊
創建jest.setup.ts文件引入斷言庫:
import "@testing-library/jest-dom";
TypeScript配置
修改tsconfig.json包含測試相關文件:
{
"include": [
"src",
"jest.config.ts",
"jest.setup.ts",
"__mocks__/**/*.ts"
]
}
測試用例編寫
我們以一個通知組件為例,該組件有兩種UI形態:
- 標題和描述組合的時間內容文案提示
- 帶有喇叭圖標的提示,點擊關閉按鈕時調用接口保存用戶狀態
![]()
特殊依賴處理
組件中有三類特殊引入需要處理:
import './index.less';
import { noticeIcon, closeIcon } from "$src/common/icon";
import request from "$src/request";
1、處理 CSS/LESS 資源
Jest 默認無法解析 CSS/LESS 文件,我們可以通過配置將其模擬為空對象:
// jest.config.js
module.exports = {
moduleNameMapper: {
"\\.(less|css)$': '<rootDir>/__mocks__/styleMock.ts", // 指向一個空文件
},
};
// __mocks__/styleMock.ts
module.exports = {};
2、配置路徑別名
對于 $src 這樣的路徑別名,需要在 Jest 配置中映射:
// jest.config.js
module.exports = {
moduleNameMapper: {
'^\\$src/(.*)$': '<rootDir>/src/$1',
},
};
3、模擬圖標資源
對于圖標這類靜態資源,我們可以在測試文件中直接模擬:
// __tests__/index.test.tsx
jest.mock('$src/common/icon', () => ({
noticeIcon: 'notice-icon-path',
closeIcon: 'close-icon-path',
}));
4、模擬 API 請求
對于網絡請求模塊,我們可以將其轉換為 Jest 模擬函數:
// __tests__/index.test.tsx
import request from '$src/request';
const mockedRequest = request as jest.MockedFunction<typeof request>;
jest.mock('$src/request', () => ({
__esModule: true, // 標識這是 ES Module
default: jest.fn(() => Promise.resolve({ data: {} })),
}));
通過以上配置,我們能夠有效地隔離組件測試環境,專注于組件邏輯本身的測試,而不受樣式、靜態資源和網絡請求等外部因素的影響。
基礎測試框架搭建
我們首先建立測試的基本結構:
describe("Notification組件", () => {
// 公共props定義
const baseProps = {
body: {},
tokenId: "test-token",
urlPrefix: "https://api.example.com",
};
// 每個測試用例前的清理工作
beforeEach(() => {
jest.clearAllMocks();
mockedRequest.mockReset();
});
});
核心測試場景覆蓋
在配置好 Jest 測試環境后,我們將針對通知組件編寫全面的測試用例。該組件具有兩種展示形態和交互邏輯,我們將從四個關鍵維度進行測試覆蓋:
1、邊界情況測試
我們首先考慮最極端的場景——當傳入無效props時,組件是否能夠優雅處理:
it("當傳入無效body時應安全地返回null", () => {
const { container } = render(<Notification {...baseProps} body={null} />);
expect(container.firstChild).toBeNull();
});
2、日期類型展示驗證
對于日期類型的通知,我們需要確認:
- 關鍵文本是否正確渲染
- DOM結構是否符合預期
- 樣式類是否準確應用
it("應正確渲染日期類型通知", () => {
render(<Notification {...dateProps} />);
expect(screen.getByText("今日公告")).toBeInTheDocument();
expect(screen.getByText("2023-06-15")).toBeInTheDocument();
const dateContainer = screen.getByText("今日公告").parentElement;
expect(dateContainer).toHaveClass("notice-header-date");
});
3、廣播類型交互測試
廣播通知的測試更加復雜,需要驗證:
- 初始狀態下的元素展示
- 圖標資源是否正確加載
- 點擊關閉后的行為
describe("BROADCAST_TYPE 類型", () => {
const broadcastProps = {
...baseProps,
body: {
type: BROADCAST_TYPE,
content: "重要通知內容",
closeUrl: "/close-notice",
},
};
it("初始狀態下應該顯示廣播內容", () => {
render(<Notification {...broadcastProps} />);
// 驗證內容
expect(screen.getByText("重要通知內容")).toBeInTheDocument();
// 驗證圖片
const images = screen.getAllByRole("img");
expect(images[0]).toHaveAttribute("src", "notice-icon-path");
expect(images[1]).toHaveAttribute("src", "close-icon-path");
// 驗證類名
const broadcastContainer = screen
.getByText("重要通知內容")
.closest(".notice-header-broadcast");
expect(broadcastContainer).toBeInTheDocument();
});
it("點擊關閉按鈕后應該隱藏廣播內容", () => {
render(<Notification {...broadcastProps} />);
// 找到關閉按鈕(假設是最后一個img元素)
const closeButton = screen.getAllByRole("img")[1].parentElement;
fireEvent.click(closeButton!);
expect(screen.queryByText("重要通知內容")).not.toBeInTheDocument();
});
});
4、網絡請求場景全覆蓋
對于涉及API調用的場景,我們設計了多維度測試:
- 正常請求流程
- 無請求場景請求
- 失敗處理
- 請求中的狀態管理
describe("網絡請求測試", () => {
const broadcastPropsWithCloseUrl = {
...baseProps,
body: {
type: BROADCAST_TYPE,
content: "重要通知內容",
closeUrl: "/close-notice",
},
};
const broadcastPropsWithoutCloseUrl = {
...baseProps,
body: {
type: BROADCAST_TYPE,
content: "重要通知內容",
// 沒有closeUrl
},
};
it("點擊關閉時應該發送請求", async () => {
// 模擬請求成功
mockedRequest.mockResolvedValue({ data: {} });
render(<Notification {...broadcastPropsWithCloseUrl} />);
const closeButton = screen.getAllByRole("img")[1].parentElement;
await act(async () => {
fireEvent.click(closeButton!);
});
//驗證請求參數
expect(request).toHaveBeenCalledWith({
url: "https://api.example.com/close-notice",
method: "post",
data: {},
headers: {
tokenId: "test-token",
},
});
// 驗證UI更新
expect(screen.queryByText("重要通知內容")).not.toBeInTheDocument();
});
it("當沒有closeUrl時不發送請求", async () => {
render(<Notification {...broadcastPropsWithoutCloseUrl} />);
const closeButton = screen.getAllByRole("img")[1].parentElement;
await act(async () => {
fireEvent.click(closeButton!);
});
expect(request).not.toHaveBeenCalled();
// 驗證UI仍然會更新
expect(screen.queryByText("重要通知內容")).not.toBeInTheDocument();
});
it("請求失敗時仍然關閉通知", async () => {
// 模擬請求失敗
mockedRequest.mockResolvedValue(new Error("Request failed"));
render(<Notification {...broadcastPropsWithCloseUrl} />);
const closeButton = screen.getAllByRole("img")[1].parentElement;
await act(async () => {
fireEvent.click(closeButton!);
});
// 驗證即使請求失敗,UI也會更新
expect(screen.queryByText("重要通知內容")).not.toBeInTheDocument();
expect(request).toHaveBeenCalled();
});
it("請求期間UI應保持響應", async () => {
// 創建一個未立即resolve的Promise
let resolveRequest: any;
const promise = new Promise((resolve) => {
resolveRequest = resolve;
});
mockedRequest.mockReturnValue(promise);
render(<Notification {...broadcastPropsWithCloseUrl} />);
const closeButton = screen.getAllByRole("img")[1].parentElement;
// 第一次點擊
fireEvent.click(closeButton!);
// 驗證UI已立即更新
expect(screen.queryByText("重要通知內容")).not.toBeInTheDocument();
// 完成請求
await act(async () => {
resolveRequest({ data: {} });
});
});
測試執行與覆蓋率
基礎測試執行
在完成通知組件的測試用例編寫后,可以在 package.json 中配置測試腳本:
{
"scripts": {
"test": "jest"
}
}
執行 npm run test 命令后,如下圖所示,Jest 會在終端輸出測試結果,包括:
- 測試文件數量
- 通過的測試用例數
- 失敗的測試用例詳情(包含錯誤堆棧信息)

覆蓋率報告配置
為了更全面地評估測試質量,可以通過修改 jest.config.ts 啟用覆蓋率統計:
module.exports = {
collectCoverage: true, // 啟用覆蓋率收集
coverageDirectory: "coverage", // 指定覆蓋率報告的輸出目錄
coverageReporters: ["text", "html", "lcov", "clover"], //指定生成的覆蓋率報告格式
coverageThreshold: {
// 設置覆蓋率的最低閾值,如果未達標,Jest 會報錯
global: {
// 全局覆蓋率要求
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
"./src/components/**/*.tsx": {
// 針對特定目錄/文件設置更高要求
branches: 90,
functions: 90,
lines: 90,
statements: 90,
},
},
}
執行測試后:終端會顯示各維度的覆蓋率百分比,在 coverage/ 目錄下生成詳細報告:index.html 提供可視化分析可逐層查看未覆蓋的代碼路徑。

示例輸出中顯示 common/util.ts 僅 32.39% 覆蓋率,低于預設閾值。此時應該優先補充核心工具函數的測試用例。通過持續完善測試覆蓋,可以有效提升組件迭代的可靠性,并為后續重構提供安全保障。
通過引入自動化測試,我們實現了從"人肉測試"到系統化保障的轉變。精心設計的測試用例覆蓋了各種邊界情況,配合覆蓋率分析,構建了多層次的質量防護體系。
如果你對前端工程化有興趣,或者想了解更多相關的內容,歡迎查看我的其他文章,這些內容將持續更新,希望能給你帶來更多的靈感和技術分享~


浙公網安備 33010602011771號