面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月27日 19:51

什么是 Jest 测试框架?它有哪些核心特性?

Jest 是由 Facebook(Meta)开发的 JavaScript 测试框架,凭借零配置、内置工具链和出色的开发者体验,已成为前端领域使用率最高的测试框架。根据 State of JS 调查,Jest 的使用率从 2016 年的 8% 增长到 2021 年的 73%,被 Facebook、Airbnb、Spotify 等公司广泛采用。核心特性1. 零配置开箱即用Jest 内置了测试运行器、断言库、Mock 系统、代码覆盖率工具和快照测试功能,无需安装和配置额外依赖即可开始编写测试:npm install --save-dev jest在 package.json 中添加测试脚本后即可运行:{ "scripts": { "test": "jest" }}2. 内置断言库与丰富的匹配器Jest 提供了 expect 断言函数和大量匹配器(matchers),覆盖常见断言场景:test('基础匹配器示例', () => { // 相等性判断 expect(1 + 1).toBe(2); // 严格相等 === expect({ a: 1 }).toEqual({ a: 1 }); // 深度相等 // 真值判断 expect(true).toBeTruthy(); expect(null).toBeFalsy(); expect(undefined).toBeUndefined(); expect(1).toBeDefined(); // 数字比较 expect(0.1 + 0.2).toBeCloseTo(0.3); // 浮点数近似 expect(5).toBeGreaterThan(3); // 字符串匹配 expect('hello world').toMatch(/world/); // 数组与异常 expect([1, 2, 3]).toContain(2); expect(() => { throw new Error('err'); }).toThrow('err');});3. 强大的 Mock 系统Mock 是 Jest 最核心的能力之一,可以模拟函数、模块和定时器,隔离被测代码的外部依赖:// 模拟函数const mockFn = jest.fn();mockFn.mockReturnValue(42);mockFn(); // 返回 42// 验证调用情况expect(mockFn).toHaveBeenCalled();expect(mockFn).toHaveBeenCalledTimes(1);// 模拟模块jest.mock('axios');axios.get.mockResolvedValue({ data: { name: 'test' } });// 模拟实现const calc = jest.fn((a, b) => a + b);calc(1, 2); // 返回 3expect(calc).toHaveBeenCalledWith(1, 2);4. 快照测试快照测试用于捕获组件或函数的输出,在后续运行中对比是否发生变化,特别适合 UI 组件测试:test('按钮组件快照', () => { const { container } = render(<Button label="Submit" />); expect(container).toMatchSnapshot();});首次运行会生成 .snap 快照文件,后续运行自动对比。如果变化是预期的,使用 jest --updateSnapshot 更新。5. 并行执行与性能优化Jest 自动并行执行测试文件,利用 Worker 进程充分使用多核 CPU,显著提高测试速度。还支持以下优化策略:--onlyChanged:只运行受改动影响的测试文件--findRelatedTests:运行与指定文件相关的测试缓存机制:自动缓存未变更文件的测试结果6. 内置代码覆盖率无需额外安装 Istanbul 等工具,Jest 内置覆盖率统计:jest --coverage可生成行覆盖率、分支覆盖率、函数覆盖率和语句覆盖率报告,支持 HTML 可视化输出。核心概念测试组织结构describe('Calculator', () => { beforeAll(() => { /* 所有测试前执行一次 */ }); afterAll(() => { /* 所有测试后执行一次 */ }); beforeEach(() => { /* 每个测试前执行 */ }); afterEach(() => { /* 每个测试后执行 */ }); test('adds two numbers', () => { expect(add(1, 2)).toBe(3); }); test('subtracts two numbers', () => { expect(subtract(5, 3)).toBe(2); });});describe:将相关测试用例分组,支持嵌套test/it:定义单个测试用例钩子函数:beforeAll/afterAll/beforeEach/afterEach 管理测试生命周期异步测试Jest 支持多种异步测试方式:// 回调方式test('callback', (done) => { fetchData((data) => { expect(data).toBe('result'); done(); });});// Promise 方式test('promise', () => { return fetchData().then(data => { expect(data).toBe('result'); });});// async/await 方式(推荐)test('async/await', async () => { const data = await fetchData(); expect(data).toBe('result');});// resolves/rejects 匹配器test('resolves matcher', () => { return expect(fetchData()).resolves.toBe('result');});面试常见追问Jest 的测试隔离机制是什么?每个测试文件在独立的模块作用域中执行,beforeEach/afterEach 确保测试之间状态不共享。Jest 通过 jest.isolateModules() 或自动的模块注册表隔离来防止测试间污染。spyOn 和 jest.fn() 有什么区别?jest.fn() 创建全新的模拟函数,不保留原始实现jest.spyOn(object, method) 包装对象的现有方法,保留原始实现,可通过 .mockImplementation() 替换,用 .mockRestore() 恢复const spy = jest.spyOn(console, 'log').mockImplementation(() => {});// 测试结束后恢复spy.mockRestore();快照测试的局限是什么?快照可能过于宽泛,导致即使有 bug 也通过对比大型快照难以 review,容易盲目更新不适合频繁变更的 UI 或动态数据最佳实践:保持快照小而精确,使用 toMatchSnapshot 配合自定义匹配器。与其他框架对比| 特性 | Jest | Mocha | Vitest ||------|------|-------|--------|| 配置 | 零配置 | 需搭配 chai/sinon/nyc | 兼容 Jest API,零配置 || 断言库 | 内置 | 需额外安装 | 内置 || Mock | 内置 | 需搭配 Sinon | 内置 || 快照测试 | 内置 | 需额外插件 | 内置 || 执行速度 | 快(并行) | 较慢 | 最快(ESM 原生) || ESM 支持 | 实验性 | 支持 | 原生支持 || 生态成熟度 | 最成熟 | 成熟 | 快速增长 |Vitest 是 Jest 的新兴替代,与 Vite 生态深度整合,在 ESM 原生支持和执行速度上有优势,但 Jest 的生态和社区资源仍然最为丰富。总结Jest 的核心优势在于"一站式"测试体验——内置断言、Mock、快照和覆盖率,零配置即可运行,并行执行保证速度。面试中需重点掌握 Mock 系统(jest.fn/jest.spyOn/jest.mock)、异步测试三种方式和快照测试原理。在新项目中如果使用 Vite,可以优先考虑 Vitest 作为替代。
服务端阅读 05月27日 19:50

Jest Mock 怎么用?从 Mock 函数到模块替换全解析

为什么需要 Mock?在单元测试中,被测代码往往依赖外部模块(如 API 请求、数据库、第三方库)。直接调用这些依赖会导致测试变慢、不稳定、难以控制返回值。Jest 的 Mock 功能可以替换依赖的行为,让测试专注于被测逻辑本身。一、创建 Mock 函数jest.fn() 是创建 Mock 函数最基本的方式,它会生成一个空函数并记录所有调用信息:const mockFn = jest.fn();mockFn('hello');mockFn('world');console.log(mockFn.mock.calls);// [['hello'], ['world']]console.log(mockFn.mock.results);// [{ type: 'return', value: undefined }, { type: 'return', value: undefined }]mockFn.mock 对象包含三个关键属性:| 属性 | 说明 ||------|------|| mock.calls | 每次调用的参数列表 || mock.results | 每次调用的返回值 || mock.instances | 每次调用时的 this 值 |二、控制 Mock 返回值mockReturnValue — 固定返回值const getAge = jest.fn().mockReturnValue(25);console.log(getAge()); // 25console.log(getAge()); // 25(每次都返回相同值)mockReturnValueOnce — 一次性返回值const getRandom = jest.fn() .mockReturnValueOnce(1) .mockReturnValueOnce(2) .mockReturnValue(0);console.log(getRandom()); // 1console.log(getRandom()); // 2console.log(getRandom()); // 0(Once 用完后回落到 mockReturnValue)mockResolvedValue — 异步返回值const fetchUser = jest.fn().mockResolvedValue({ name: 'Alice' });// 在测试中使用 async/awaitconst user = await fetchUser(1);expect(user).toEqual({ name: 'Alice' });mockResolvedValueOnce 同理,仅生效一次。三、自定义 Mock 实现当需要根据参数动态返回不同值时,使用 mockImplementation:const calculate = jest.fn().mockImplementation((a, b) => a + b);expect(calculate(1, 2)).toBe(3);也可以在 jest.fn() 中直接传入实现:const greet = jest.fn(name => `Hello, ${name}!`);进阶用法 — 根据调用次数返回不同值:const fn = jest.fn() .mockImplementationOnce(() => 'first') .mockImplementationOnce(() => 'second') .mockImplementation(() => 'default');四、Mock 整个模块这是实际项目中最常用的场景 — 替换外部模块的导出:替换默认导出// api.jsexport default function fetchData() { return fetch('/api/data');}// __tests__/component.test.jsjest.mock('../api', () => ({ __esModule: true, default: jest.fn(() => Promise.resolve({ data: 'mocked' }))}));import fetchData from '../api';test('使用模拟的 API 数据', async () => { const result = await fetchData(); expect(result).toEqual({ data: 'mocked' });});替换命名导出// utils.jsexport function formatDate(date) { /* ... */ }export function parseJSON(str) { /* ... */ }// 仅 Mock formatDate,保留 parseJSON 原始实现(Partial Mock)jest.mock('../utils', () => ({ ...jest.requireActual('../utils'), formatDate: jest.fn(() => '2026-01-01')}));使用 __mocks__ 目录自动 Mock在模块同目录下创建 __mocks__/api.js:// __mocks__/api.jsexport default function fetchData() { return Promise.resolve({ data: 'from automock' });}测试文件只需声明 jest.mock('../api'),Jest 会自动查找 __mocks__ 目录。五、SpyOn — 监视真实函数jest.spyOn 在不替换原函数的情况下追踪调用,也可以按需 Mock:const math = { add: (a, b) => a + b,};test('spy 追踪调用但不改变行为', () => { const spy = jest.spyOn(math, 'add'); expect(math.add(1, 2)).toBe(3); // 原函数正常执行 expect(spy).toHaveBeenCalledWith(1, 2); // 同时记录了调用});test('spy 也可以临时替换实现', () => { jest.spyOn(math, 'add').mockReturnValue(999); expect(math.add(1, 2)).toBe(999); // 被替换了 math.add.mockRestore(); // 恢复原函数});六、常用断言| 断言 | 说明 ||------|------|| toHaveBeenCalled() | 至少被调用一次 || toHaveBeenCalledTimes(n) | 被调用了 n 次 || toHaveBeenCalledWith(...args) | 曾用指定参数调用 || toHaveBeenLastCalledWith(...args) | 最后一次调用的参数 || toHaveReturnedWith(value) | 曾返回指定值 || toHaveLastReturnedWith(value) | 最后一次返回的值 || toHaveReturnedTimes(n) | 成功返回了 n 次 |七、清理 Mock测试之间未清理的 Mock 会导致状态泄漏,务必在 afterEach 或 afterAll 中清理:afterEach(() => { jest.clearAllMocks(); // 清除所有 mock.calls、mock.results,但保留实现});afterAll(() => { jest.restoreAllMocks(); // 恢复所有 spyOn 的原始实现});| 方法 | 效果 ||------|------|| jest.clearAllMocks() | 清除调用记录,保留 mock 实现 || jest.resetAllMocks() | 清除调用记录 + 清除 mock 实现(恢复为空函数) || jest.restoreAllMocks() | 恢复 spyOn 的原始实现 |八、常见问题与最佳实践问题1:Mock 不生效jest.mock 会被提升(hoisted)到文件顶部,如果回调中使用了变量,该变量可能尚未定义。解决方案:// 错误 — mockFactory 尚未定义const mockFactory = () => jest.fn();jest.mock('../module', mockFactory);// 正确 — 使用动态函数jest.mock('../module', () => ({ myMethod: jest.fn()}));问题2:Timer Mock测试 setTimeout、setInterval 相关逻辑时:jest.useFakeTimers();test('延迟执行', () => { const callback = jest.fn(); setTimeout(callback, 1000); jest.advanceTimersByTime(1000); expect(callback).toHaveBeenCalled();});最佳实践Mock 外部依赖,不 Mock 被测代码本身 — 否则测试失去意义优先使用 spyOn 而非 jest.fn 替换 — 便于恢复原始行为每个测试前确保 Mock 状态干净 — 避免测试间相互影响Mock 的行为应尽量贴近真实 — 否则测试通过但代码可能在生产环境失败不要过度 Mock — 如果一个测试中 Mock 了超过 3 个依赖,考虑是否测试粒度不对
服务端阅读 05月27日 19:50

Jest 生命周期钩子有哪些?beforeAll、afterAll、beforeEach 和 afterEach 怎么用?

Jest 提供了四个生命周期钩子函数,用于在测试的不同阶段执行设置和清理操作。理解它们的执行时机和使用场景,是编写可靠测试的基础。四个钩子函数概览| 钩子 | 执行时机 | 典型用途 ||------|---------|---------|| beforeAll | 当前 describe 块所有测试运行前,仅执行一次 | 建立数据库连接、启动服务器 || afterAll | 当前 describe 块所有测试运行后,仅执行一次 | 关闭数据库连接、停止服务器 || beforeEach | 当前 describe 块每个测试运行前,每次都执行 | 重置状态、初始化数据 || afterEach | 当前 describe 块每个测试运行后,每次都执行 | 清除 Mock、还原定时器 |beforeAll 与 afterAll:一次性设置与清理beforeAll 适合需要一次投入成本的场景,避免在每个测试前重复执行:let db;beforeAll(async () => { db = await connectDatabase('test_db'); await db.createTables();});afterAll(async () => { await db.dropTables(); await db.close();});test('should insert user', async () => { await db.insert('users', { name: 'Alice' }); const users = await db.query('SELECT * FROM users'); expect(users).toHaveLength(1);});注意: beforeAll 中如果发生错误,该 describe 块内的所有测试都会失败。beforeEach 与 afterEach:逐测试隔离beforeEach 和 afterEach 保证每个测试在独立环境中运行,是测试隔离的核心手段:let users;beforeEach(() => { users = [{ id: 1, name: 'Alice' }];});afterEach(() => { jest.clearAllMocks(); jest.useRealTimers();});test('should add user', () => { users.push({ id: 2, name: 'Bob' }); expect(users).toHaveLength(2);});test('should not be affected by previous test', () => { // beforeEach 重置了 users,这里仍然是初始状态 expect(users).toHaveLength(1);});钩子的执行顺序当存在嵌套 describe 时,钩子按照从外到内的顺序执行 setup,从内到外的顺序执行 teardown:describe('Outer', () => { beforeAll(() => console.log('Outer beforeAll')); beforeEach(() => console.log('Outer beforeEach')); afterEach(() => console.log('Outer afterEach')); afterAll(() => console.log('Outer afterAll')); describe('Inner', () => { beforeAll(() => console.log('Inner beforeAll')); beforeEach(() => console.log('Inner beforeEach')); afterEach(() => console.log('Inner afterEach')); afterAll(() => console.log('Inner afterAll')); test('example', () => { console.log('--- test runs ---'); }); });});执行顺序输出:Outer beforeAllInner beforeAllOuter beforeEachInner beforeEach--- test runs ---Inner afterEachOuter afterEachInner afterAllOuter afterAll关键规则: 外层 beforeEach 先于内层执行,外层 afterEach 后于内层执行——这保证了内层可以依赖外层的设置,同时内层的清理不会影响外层。异步钩子钩子函数支持异步操作,三种写法均可:// 方式一:async/await(推荐)beforeAll(async () => { await initializeService();});// 方式二:返回 PromisebeforeAll(() => { return fetch('/api/setup').then(res => res.json());});// 方式三:单个参数 done 回调beforeAll((done) => { startServer(done);});如果异步钩子超时,可以设置自定义超时时间:beforeAll(async () => { await heavySetup();}, 30000); // 30 秒超时常见陷阱1. 在 beforeAll 中修改共享状态,在 afterEach 中忘记清理// 错误:beforeAll 修改了全局状态,但 afterAll 没有还原beforeAll(() => { process.env.NODE_ENV = 'test';});// 其他测试文件可能受到影响// 正确:配对使用 afterAll 还原beforeAll(() => { originalEnv = process.env.NODE_ENV; process.env.NODE_ENV = 'test';});afterAll(() => { process.env.NODE_ENV = originalEnv;});2. 混淆 beforeAll 和 beforeEach 的使用场景beforeAll:设置成本高、测试之间可共享(数据库连接、服务器启动)beforeEach:每个测试需要独立副本(状态重置、数据初始化)如果测试之间有依赖或顺序敏感,优先使用 beforeEach 保证隔离。3. 钩子中的错误导致测试全部跳过beforeAll 抛出错误时,该 describe 块内所有测试直接失败。如果部分初始化失败不应阻断所有测试,考虑将初始化移入 beforeEach 并做容错处理。最佳实践优先使用 beforeEach/afterEach 保证隔离,仅在设置成本确实很高时才用 beforeAllafterEach 中务必清理 Mock 和定时器:jest.clearAllMocks() + jest.useRealTimers()保持钩子函数简洁,复杂逻辑拆分为辅助函数配对使用:有 before 就有对应的 after,确保资源不泄漏避免在钩子间传递状态,每个测试应能独立运行