跳转至正文

JavaScript 单元测试:2026 全方位指南

用我们的 2026 指南精通 JavaScript 单元测试。涵盖 Jest、Mocha、环境搭建、Mock、CI,以及 Capacitor 和 Electron 应用的技巧。

《JavaScript 单元测试:2026 全面指南》 Capgo 博客插图

你现在可能正处于两种情况之一:要么你的 JavaScript 项目几乎没有测试,每次重构都像在冒险;要么你已经有测试,但其中一半运行缓慢、很脆弱,还莫名让人难以信任。

在 Capacitor 以及 Electron 应用中,情况会更糟。一个简单的功能可能会在同一流程中涉及共享业务逻辑、浏览器 API、原生插件、本地文件、IPC 和远程服务。如果测试方式不对,测试套件就会变成充满假依赖的迷宫;如果测试得当,就能快速获得关于出问题逻辑的反馈。

好的 JavaScript 单元测试不是从花哨的匹配器语法开始的,而是从清晰的边界开始:直接测试纯逻辑,隔离副作用,别写那种内部函数一改名就失效的测试。

目录

如何选择你的 JavaScript 测试框架

专业的 JavaScript 项目需要一个真正的测试运行器。临时脚本和手动控制台检查在多名工程师共同维护同一代码库时无法扩展。你需要测试发现、断言、异步处理、mock,以及一种在本地开发和 CI 中一致运行所有测试的方法。

当前的指导方针正逐渐收敛到少数几个主流选项。 Jest、Mocha 和 Jasmine 被反复强调为主要框架,并且 Jest 常因在一个包中内置测试结构、断言、Mock 和异步支持而备受推崇,如下所示: Pluralsight JavaScript 测试实验.

展示热门 JavaScript 测试框架的对比图,包括 Jest、Mocha、Cypress 和 Playwright。

为什么框架是必不可少的

团队常犯的第一个错误就是把单元测试当作次要任务。这通常会导致文件命名不统一、没人记得住的自定义断言,以及只有一个人能看懂的辅助函数。

测试框架为你提供了一套通用的语言:

  • 测试结构 配合 describe 以及 test 或 it
  • 断言 搭配可读的匹配器
  • Hooks 用于 setup 和 teardown
  • 异步支持 用于处理 promises 和 timers
  • Mock 工具 用于外部依赖

如果你的团队还需要在单元测试之外更全面地了解测试自动化,Capgo 提供了一份实用概览,涵盖 应用交付工作流中的自动化测试.

Jest vs Mocha 快速对比

Jest 和 Mocha 代表了两种不同的设计理念。

Jest 是全能型方案。它自带团队一开始需要的大部分功能。
Mocha 更加模块化。它只提供一个测试运行器,需要你自行搭建其余的技术栈。

功能 Jest Mocha
配置复杂度 对大多数团队更低 更高,因为通常要额外加断言和 Mock 库
断言 内置 通常需要配合其他库使用
Mock 测试 内置 通常需要配合其他库使用
异步测试 内置且直观 支持,但更依赖周边配置
覆盖率工作流 通常集成在同一工具链中 往往更零散
最合适 新项目,追求一致性的团队 遗留技术栈,追求模块化控制的团队

实战规则: 如果你的团队还得问该用哪个断言库和 mock 库来搭配 runner,那你大概率需要 Jest。

我为大多数团队推荐的方案

对于大多数现代项目,我会选择 Jest 除非代码库已有充分理由继续使用 Mocha。当应用包含以下内容时,这个建议会更有说服力: Capacitor 或 Electron,因为这些项目本身已经够复杂了。减少测试工具泛滥能很快见效。

在较旧的 Node.js 服务或周边生态已经稳定的长期代码库中,Mocha 仍然说得通。但对于从零开始搭建可靠测试套件的中级工程师来说,Jest 通常减少的摩擦比带来的更多。

一个重要范围说明:Cypress 和 Playwright 是很棒的工具,但它们解决的是另一个问题。它们更适合浏览器级和端到端检查,而不是 JavaScript 单元测试所在的快速内循环。

项目设置与第一个测试

一套干净的测试配置应该平淡无奇。如果添加第一个测试都显得复杂,这套测试套件可能很难长期保持健康。

一名戴眼镜的男子坐在木桌前,用笔记本电脑做编程项目。

一个简单的 Jest 配置

从一个已经具备以下内容的 JavaScript 项目开始 package.json。然后将 Jest 添加为开发依赖项,并配置测试脚本。

{
  "scripts": {
    "test": "jest"
  }
}

对于许多项目来说,这就足够了。如果模块系统、转译或 monorepo 结构需要,你可以稍后添加更多配置。

如果你正在本地构建 Capacitor 应用,并希望在进行共享逻辑测试之前整理好开发环境,Capgo 的指南 设置 Capacitor 本地环境 是一个实用的参考。

在编写 code 之前先写测试

测试优先模式不仅仅是个人偏好。美国消费者金融保护局的 JavaScript 指南明确推荐 先写测试,用……组织测试 describe 以及 it,并围绕……设置检查 expect(...) 其……中的断言 JavaScript 单元测试指南.

这很重要,因为测试先行会改变你设计 code 的方式。函数往往变得更小,依赖关系更明确,副作用也不会再渗入本应保持纯粹的逻辑。

下面是一个最小示例:

// math.js
function addTax(amount, rate) {
  return amount + amount * rate;
}

module.exports = { addTax };
// math.test.js
const { addTax } = require('./math');

describe('addTax', () => {
  it('returns the amount with the tax applied', () => {
    expect(addTax(100, 0.2)).toBe(120);
  });
});

始终使用 Arrange-Act-Assert 模式

本指南中介绍的 准备、执行、断言 该模式能让测试保持可读,即使测试变得更复杂。

  1. 准备 准备输入以及任何必要的设置。
  2. 执行 通过调用函数来执行。
  3. 断言 对结果。

应用于校验辅助函数:

function isSupportedPlatform(platform) {
  return ['ios', 'android', 'web', 'desktop'].includes(platform);
}

describe('isSupportedPlatform', () => {
  it('returns true for ios', () => {
    // Arrange
    const platform = 'ios';

    // Act
    const result = isSupportedPlatform(platform);

    // Assert
    expect(result).toBe(true);
  });
});

小型测试更经得起时间考验。一个测试通常应该只回答一个问题,而不是叙述整个工作流。

对于 Capacitor 和 Electron 项目,这种纪律更重要,因为你的纯逻辑往往紧挨着原生或桌面集成的 code。让业务规则无需平台运行时也能测试,你的第一个测试就不会是最后一个有用的测试。

掌握 Mock 与异步 Code

应用 code 中的大多数 bug 并不是来自两个数字相加,而是来自会触及自身之外的 code:网络请求、文件、插件 API、定时器、IPC 通道、存储层。

这正是 mock 发挥作用的地方。它让你能够控制边界,从而使测试可以专注于 code 的决策逻辑。

一张展示微服务架构的白板图,包含 API、数据存储、外部服务和事件驱动的数据流。

只 mock 边界,而非全部

可维护的测试指南强调 单一行为覆盖 以及 每个测试仅包含一个核心断言,同时它还警告说过度使用 mock 会导致测试变得脆弱,并与实现细节紧密耦合,正如这篇 关于可维护单元测试的 TestRail 文章所述.

这种警告在 JavaScript 中很重要。团队常常一开始 mock 所有导入的模块,最终却变成测试函数是否按“正确”顺序调用其他函数,而不是测试真实行为。

不适合进行重度 mock 测试的目标:

  • 辅助函数 A 是否调用了辅助函数 B
  • 服务 C 是否调用了序列化器 D
  • 内部私有函数是否执行了两次

更好的测试目标:

  • 函数的返回值是什么
  • 它是否正确处理了依赖项失败的情况
  • 它是否将数据转换成了预期的结构

Capacitor 和 Electron code 的更好模式

在移动端和桌面端应用中,我更倾向于在原生或平台 API 之上封装一层。这样单元测试时只需 mock 这个封装层,而无需 mock 平台本身。

示例结构:

// cameraGateway.js
async function getPhoto(cameraPlugin) {
  return cameraPlugin.getPhoto();
}

module.exports = { getPhoto };
// profilePhotoService.js
async function loadProfilePhoto(cameraGateway) {
  const photo = await cameraGateway.getPhoto();
  return { path: photo.path, ready: true };
}

module.exports = { loadProfilePhoto };
// profilePhotoService.test.js
const { loadProfilePhoto } = require('./profilePhotoService');

test('returns mapped photo data', async () => {
  const fakeCameraGateway = {
    getPhoto: jest.fn().mockResolvedValue({ path: '/tmp/pic.jpg' })
  };

  const result = await loadProfilePhoto(fakeCameraGateway);

  expect(result).toEqual({ path: '/tmp/pic.jpg', ready: true });
});

这种模式同样适用于 Electron。把 ipcRenderer文件访问或 Shell 集成封装在轻量适配器后面。单元测试针对服务层,而不是直接针对运行时。

对于在 Capacitor 应用中测试发布逻辑和更新路径的团队,Capgo 有一份相关指南,介绍 如何通过 mock 场景测试 Capacitor OTA 更新.

如果你的团队还在统一异步测试写法,快速过一遍会有帮助:

测试异步流程,避免 flaky

使用 async/await 在测试中,当被测 code 返回 Promise 时,这比回调密集的模式更清晰,也更容易调试。

async function fetchProfile(api) {
  const response = await api.getUser();
  return response.name;
}

test('returns the user name from the API response', async () => {
  const api = {
    getUser: jest.fn().mockResolvedValue({ name: 'Ava' })
  };

  const result = await fetchProfile(api);

  expect(result).toBe('Ava');
});

同时也要测试失败路径:

test('throws when the API request fails', async () => {
  const api = {
    getUser: jest.fn().mockRejectedValue(new Error('network failed'))
  };

  await expect(fetchProfile(api)).rejects.toThrow('network failed');
});

既要测试正常路径,也要测试异常路径。在生产环境中,用户记住的往往是异常路径。

稳健测试的高级策略

测试套件只有在 code 变化后依然有效,才真正有用。这比写一堆能通过的测试难多了。

展示如何通过全面的测试覆盖和可维护的测试套件构建健壮软件的策略示意图。

把测试拆分当作预算

一份实用指南建议将测试 70/20/10 拆分为 单元测试、集成测试和端到端测试,其中单元测试反馈最快,失败结果也最稳定。该指南还指出,完整的单元测试套件理想情况下应在 10 秒内完成,而 pre-commit 检查应保持在 5 秒以内,根据这一点 OpenReplay 测试指南.

我把测试看作一种预算管理工具,而不是教条。如果你的精力大多花在端到端测试上,团队获取反馈的速度会太慢;如果只做单元测试,你又会忽略真实的系统边界。

对于 Capacitor 或 Electron 应用,合理的平衡通常如下:

  • 单元测试 用于定价逻辑、权限规则、序列化、更新资格、功能开关和状态转换
  • 集成测试 用于存储适配器、插件封装和 IPC 契约
  • E2E 测试 用于登录、购买流程、同步或更新提示等几个关键路径

覆盖率是手电筒,不是目标

当覆盖率报告能帮你发现重要逻辑中未测试的分支时,它非常有用;但如果团队只是为了追求覆盖率百分比而盲目测试,它反而会产生副作用。

一个带有周全边界用例测试的登录校验器,比一个覆盖率高但满是琐碎断言的文件更有价值。对于表单、解析器、日期逻辑和权限检查等输入密集型 code 尤其如此。如果你的团队正在提升校验密集型 UI 的质量,那么这篇关于 精通前端表单验证 的指南是单元级测试策略的良好补充。

行为优先的测试更耐重构

一套可靠的测试套件应能让你重构内部实现,而无需重写一半的测试。做到这一点最简单的方式就是断言 可观察行为 而非实现细节。

表现良好的用例包括:

  • 边界条件 例如空输入、类 null 值、无效类型和超长字符串
  • 领域结果 例如“缺少权限时返回拒绝”
  • 状态转换 例如“在下载元数据验证通过后,将更新标记为 pending”

容易失效的使用场景:

  • 检查内部 helper 调用
  • 断言私有方法的执行顺序
  • 对调用链中的每一层都进行 mock

对于建立规范发布流程的应用团队,Capgo 关于 应用质量保证的 文章很有用,因为它把测试工作与更广泛的发布 pipeline 联系了起来。

针对 CI、Capacitor 和 Electron 应用的测试

只能在某个开发者机器上运行的测试算不上安全网,只是一种本地习惯。

CI 把 JavaScript 单元测试变成团队基础设施。每次 push、pull request 或 release 分支都能执行相同的命令,并得到相同的预期结果。对于 Capacitor 和 Electron 项目,这种一致性更重要,因为环境漂移会导致隐蔽的失败。

让 CI 成为默认执行路径

至少,你的 CI 应在每次代码变更时安装依赖并运行单元测试套件。尽量让命令与本地开发保持一致。

一个基础的 GitHub Actions 工作流可以精简如下:

name: test

on: [push, pull_request]

jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm test

这足以在代码合并到 main 分支之前,捕获错误的 import、失败的断言以及无意中引入的平台假设。

对于通过自动化 pipeline 发布应用的移动端团队,Capgo 提供了一份实用指南,用于 为 Capacitor 应用配置 CI/CD.

测试 Capacitor 插件交互

对 Capacitor code 做单元测试的错误做法,是把原生插件直接引入每个 service。这会让测试套件与平台桥接层强耦合。

更好的模式是使用薄抽象层:

// deviceStorage.js
async function saveFile(filesystem, path, data) {
  return filesystem.writeFile({ path, data });
}

module.exports = { saveFile };
// draftService.js
async function persistDraft(storage, draft) {
  await storage.save('draft.json', JSON.stringify(draft));
  return { saved: true };
}

module.exports = { persistDraft };
// draftService.test.js
const { persistDraft } = require('./draftService');

test('persists a serialized draft', async () => {
  const storage = {
    save: jest.fn().mockResolvedValue(undefined)
  };

  const result = await persistDraft(storage, { title: 'Hello' });

  expect(result).toEqual({ saved: true });
});

同样的思路也适用于相机访问、生物识别提示、推送 token 注册和网络状态。把插件调用留在适配器中,针对自己可控的接口测试应用逻辑。

测试 Electron 主进程、渲染进程和 IPC code

Electron 应用有两个重要的接缝: 主进程 code 以及 渲染进程 code。不要在测试中模糊它们。

可靠的配置通常会把以下部分分开:

  • 渲染器单元测试 用于视图模型、状态、格式化和 UI 侧业务逻辑
  • 主进程单元测试 用于菜单、文件操作和应用生命周期决策
  • IPC 契约测试 用于消息结构和预期响应

IPC 封装示例:

// ipcGateway.js
function sendSettings(ipcRenderer, payload) {
  ipcRenderer.send('settings:update', payload);
}

module.exports = { sendSettings };
// ipcGateway.test.js
const { sendSettings } = require('./ipcGateway');

test('sends settings update over ipc', () => {
  const ipcRenderer = { send: jest.fn() };

  sendSettings(ipcRenderer, { theme: 'dark' });

  expect(ipcRenderer.send).toHaveBeenCalledWith('settings:update', { theme: 'dark' });
});

即使你之后把内部实现从一个 helper 换成另一个,这个测试依然成立,因为它验证的是真正重要的行为。这也是你在桌面端和移动端 code 上应达到的标准。

JavaScript 单元测试常见问题解答

单元测试、集成测试与 E2E 测试的区别

一套 单元测试 在隔离环境中检查一小段逻辑。而 集成测试 检查几个组件或服务是否能正确协同工作。而 端到端测试(E2E) 则通过运行中的应用走一遍用户旅程。

用单元测试快速建立对业务规则的信心;用集成测试覆盖存储、plugin 封装层和 IPC 等衔接点;E2E 测试要少用,只留给那些一旦失败会带来严重影响的工作流。

我们应该追求 100% 的测试覆盖率吗?

不。追求全覆盖可能会把团队推向低价值测试。

当覆盖率能揭示没人执行过的有风险 code 时,它才有用。如果工程师只是为了满足 dashboard 而添加浅层断言,那它就没用。如果你的测试套件很脆弱,更高的覆盖率也救不了它。

如何为现有代码库添加测试

从已经在变化的地方开始。别让团队停摆,然后宣布要大规模重写测试策略。

一个实用的顺序如下:

  • 先保护活跃的 code 在开发功能或修复 bug 时,为改动到的模块添加测试
  • 提取纯逻辑 从难以测试的文件中提取纯逻辑,这样测试业务规则时就不会被 framework 或 runtime 干扰
  • 添加 seam 封装层 围绕原生插件、网络客户端、文件系统调用和 Electron IPC 添加封装层
  • 拒绝脆弱模式 在引入 mock 时,来自 JavaScript 测试最佳实践 的指南尤其有用,因为它点出了常被忽视的过度 mock 问题,以及随之而来的脆弱测试

目标不是一步到位,而是在回归代价最高的地方持续改进。


如果你的团队发布 Capacitor 或 Electron 应用,并且需要围绕 JavaScript 变更建立更清晰的发布流程, Capgo 值得考虑。它为 CapacitorJS 和 Electron 应用提供 Live Updates,并带有发布控制和可观测性,让团队既能做好扎实的单元测试,也能更安全地发布 web bundle 变更,无需每个修复都等应用商店审核。

为 Capacitor 应用提供 Live Updates

当 Web 层出现线上 Bug 时,通过 Capgo 发布修复,而不用等 App Store 审核好几天。用户会在后台收到更新,原生变更则仍走正常审核流程。

博客最新动态

Capgo 为你提供打造真正专业的移动应用所需的最有价值的洞察。