Mocking技巧:隔离外部依赖的测试策略
Mocking技巧隔离外部依赖的测试策略前言大家好我是cannonmonster01今天我们来聊聊测试中的Mocking技巧。想象一下你是一个汽车工程师在测试汽车的发动机时你不需要真的把汽车开到路上。你可以把发动机拆下来放在测试台上用模拟的输入来测试它的性能。Mocking就像是这个测试台它可以帮助我们隔离外部依赖专注于测试我们真正关心的代码。Mocking核心概念什么是MockingMocking是一种测试技术用于替换系统中的外部依赖以便我们可以控制依赖的行为验证依赖的调用隔离被测试的代码Mocking的类型类型描述Stub提供预设的返回值Mock验证调用是否发生Spy记录调用信息Fake实现简化的替代实现Mocking的场景场景描述API调用Mock HTTP请求数据库操作Mock数据库查询时间相关Mock Date.now()第三方库Mock外部库Mocking实战实战1Mock函数// utils.js export function fetchUser(userId) { return fetch(https://api.example.com/users/${userId}) .then(response response.json()); } export function formatDate(date) { return new Intl.DateTimeFormat(zh-CN).format(date); }// utils.test.js import { fetchUser, formatDate } from ./utils; jest.mock(node-fetch); import fetch from node-fetch; describe(Mocking Functions, () { beforeEach(() { fetch.mockClear(); }); it(should mock fetchUser, async () { const mockUser { id: 1, name: John }; fetch.mockResolvedValue({ json: async () mockUser, }); const user await fetchUser(1); expect(fetch).toHaveBeenCalledWith(https://api.example.com/users/1); expect(user).toEqual(mockUser); }); it(should mock Date, () { const mockDate new Date(2024-01-15); jest.spyOn(global, Date).mockImplementation(() mockDate); expect(formatDate(new Date())).toBe(2024/1/15); global.Date.mockRestore(); }); });实战2Mock模块// api.js import axios from axios; export async function getUsers() { const response await axios.get(/api/users); return response.data; } export async function createUser(user) { const response await axios.post(/api/users, user); return response.data; }// api.test.js import { getUsers, createUser } from ./api; import axios from axios; jest.mock(axios); describe(Mocking Modules, () { beforeEach(() { axios.get.mockClear(); axios.post.mockClear(); }); it(should mock axios get, async () { const mockUsers [{ id: 1, name: John }]; axios.get.mockResolvedValue({ data: mockUsers }); const users await getUsers(); expect(axios.get).toHaveBeenCalledWith(/api/users); expect(users).toEqual(mockUsers); }); it(should mock axios post, async () { const mockUser { id: 1, name: John }; const newUser { name: Jane }; axios.post.mockResolvedValue({ data: mockUser }); const result await createUser(newUser); expect(axios.post).toHaveBeenCalledWith(/api/users, newUser); expect(result).toEqual(mockUser); }); it(should mock axios error, async () { axios.get.mockRejectedValue(new Error(Network Error)); await expect(getUsers()).rejects.toThrow(Network Error); }); });实战3Mock React组件// UserCard.jsx import React from react; import Avatar from ./Avatar; function UserCard({ user }) { return ( div classNameuser-card Avatar src{user.avatar} / div classNameuser-info h3{user.name}/h3 p{user.email}/p /div /div ); } export default UserCard;// UserCard.test.jsx import React from react; import { render, screen } from testing-library/react; import UserCard from ./UserCard; import Avatar from ./Avatar; jest.mock(./Avatar); describe(Mocking React Components, () { beforeEach(() { Avatar.mockClear(); }); it(should render UserCard with mocked Avatar, () { const mockUser { id: 1, name: John Doe, email: johnexample.com, avatar: https://example.com/avatar.jpg, }; Avatar.mockReturnValue(div>// Logger.js export class Logger { constructor(name) { this.name name; } log(message) { console.log([${this.name}] ${message}); } error(message) { console.error([${this.name}] ERROR: ${message}); } } // app.js import { Logger } from ./Logger; export function processData(data) { const logger new Logger(DataProcessor); try { logger.log(Processing data...); // 处理数据 logger.log(Data processed successfully); return true; } catch (error) { logger.error(Failed to process data: ${error.message}); return false; } }// app.test.js import { processData } from ./app; import { Logger } from ./Logger; jest.mock(./Logger); describe(Mocking Classes, () { beforeEach(() { Logger.mockClear(); }); it(should mock Logger class, () { const mockLog jest.fn(); const mockError jest.fn(); Logger.mockImplementation(() ({ log: mockLog, error: mockError, })); processData({}); expect(Logger).toHaveBeenCalledWith(DataProcessor); expect(mockLog).toHaveBeenCalledTimes(2); expect(mockLog).toHaveBeenCalledWith(Processing data...); expect(mockLog).toHaveBeenCalledWith(Data processed successfully); }); it(should handle errors, () { const mockLog jest.fn(); const mockError jest.fn(); Logger.mockImplementation(() ({ log: mockLog, error: mockError, })); processData(null); expect(mockError).toHaveBeenCalled(); }); });实战5Mock定时器// debounce.js export function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later () { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout setTimeout(later, wait); }; }// debounce.test.js import { debounce } from ./debounce; describe(Mocking Timers, () { beforeEach(() { jest.useFakeTimers(); }); afterEach(() { jest.useRealTimers(); }); it(should debounce function, () { const mockFn jest.fn(); const debouncedFn debounce(mockFn, 1000); debouncedFn(hello); debouncedFn(world); debouncedFn(test); expect(mockFn).not.toHaveBeenCalled(); jest.advanceTimersByTime(1000); expect(mockFn).toHaveBeenCalledTimes(1); expect(mockFn).toHaveBeenCalledWith(test); }); it(should call function multiple times after delay, () { const mockFn jest.fn(); const debouncedFn debounce(mockFn, 500); debouncedFn(first); jest.advanceTimersByTime(500); debouncedFn(second); jest.advanceTimersByTime(500); expect(mockFn).toHaveBeenCalledTimes(2); expect(mockFn).toHaveBeenNthCalledWith(1, first); expect(mockFn).toHaveBeenNthCalledWith(2, second); }); });Mocking最佳实践1. 保持Mock简洁// 不好的做法过度复杂的Mock const mockUserService { getUser: jest.fn().mockImplementation(async (id) { if (id 1) return { id: 1, name: John }; if (id 2) return { id: 2, name: Jane }; throw new Error(User not found); }), }; // 好的做法简单的Mock根据测试场景调整 const mockUserService { getUser: jest.fn().mockResolvedValue({ id: 1, name: John }), };2. 清理Mock// 在每个测试后清理Mock afterEach(() { jest.clearAllMocks(); }); // 或者 beforeEach(() { jest.resetAllMocks(); });3. 使用mockImplementation// 当需要自定义实现时 const mockFetch jest.fn().mockImplementation((url) { if (url.includes(/users)) { return Promise.resolve({ json: () ({ users: [] }) }); } return Promise.resolve({ json: () ({}) }); });4. 验证Mock调用// 验证调用次数 expect(mockFn).toHaveBeenCalledTimes(1); // 验证调用参数 expect(mockFn).toHaveBeenCalledWith(arg1, arg2); // 验证调用顺序 expect(mockFn).toHaveBeenNthCalledWith(1, first); expect(mockFn).toHaveBeenNthCalledWith(2, second);5. 避免过度Mocking// 不好的做法Mock所有依赖 jest.mock(./utils); jest.mock(./api); jest.mock(./config); // 好的做法只Mock必要的外部依赖 jest.mock(./api); // 外部API调用需要Mock // utils和config是内部模块可以直接使用常见问题解答Q1什么时候应该使用MockA1当测试代码依赖于外部系统API、数据库、时间等时应该使用Mock来隔离这些依赖。Q2Mock和Stub有什么区别A2Stub提供预设的返回值而Mock不仅提供返回值还验证调用是否发生。Q3如何Mock全局对象A3可以使用jest.spyOn(global, 对象名)来Mock全局对象。Q4Mock会影响其他测试吗A4如果不清理可能会影响。使用jest.clearAllMocks()或jest.resetAllMocks()来清理。总结Mocking是测试中非常重要的技术它可以帮助我们隔离外部依赖专注于测试我们真正关心的代码。通过合理使用Mocking我们可以编写更加可靠、可维护的测试代码。关注我每天分享更多前端干货如果觉得这篇文章对你有帮助请点赞、收藏、转发三连支持一下