你可能处于两种情况之一。要么你的javascript项目几乎没有测试,每次重构都感到风险很大;要么你已经有了测试,但其中一半的测试很慢、脆弱且难以信任。
情况会变得更糟 Capacitor 和 Electron 应用。一个简单的功能可以触及共享的业务逻辑、浏览器API、原生插件、局部文件、IPC和远程服务在同一流程中。如果你测试那些部分的方式不对,测试套件就变成了一座迷宫般的假依赖。如果你测试它们的方式正确,你就能快速获得逻辑中出现问题的反馈。
好的单元测试JavaScript工作并不从巧妙的匹配语法开始。它从一个严格的界限开始:直接测试纯粹的逻辑,隔离副作用,并避免编写测试以便在内部函数重命名时立即崩溃。
目录
- 选择您的JavaScript测试框架
- 项目设置和您的第一个测试
- 掌握异步Code和Mock
- 高级策略:构建健壮的测试
- 测试CI、Capacitor和Electron应用
- 关于 JavaScript 单元测试的常见问题
选择 JavaScript 测试框架
一个专业的 JavaScript 项目需要一个真正的测试运行器。临时脚本和手动控制台检查在多个工程师处理同一代码库时无法扩展。您需要测试发现、断言、异步处理、模拟和在本地开发和 CI 中一致地运行所有内容的方式。
当前指导正在趋向于一小组主流选项。 Jest、Mocha 和 Jasmine 反复被突出为主要框架, Jest 经常被单独提及的内置测试结构、断言、模拟和异步支持的包装,正如本文所示 Pluralsight JavaScript 测试实验室.

为什么框架不是可选项
团队的第一个错误是将单元测试视为一项次要活动。这通常导致文件命名不一致、 nobody 记得的自定义断言和只有一个人理解的助手
框架给你一个共享的语言:
- 测试结构 有
describe和test或it - 断言 有可读的匹配器
- __CAPGO_KEEP_0__ 用于设置和清除
- 异步支持 用于Promise和定时器
- 模拟工具 用于外部依赖
如果您的团队还需要了解测试自动化的更广泛视图,除了单元测试之外的Capgo对 应用交付工作流程中的自动化测试.
Jest vs Mocha简要对比
Jest和Mocha代表了两种不同的哲学.
Jest 是全能的选择。它带来了大多数团队在第一天所需的内容。
Mocha 更模块化。它给你一个运行器并期望你自己组装整个堆栈。
| 功能 | Jest | Mocha |
|---|---|---|
| 设置复杂性 | 大多数团队中较低 | 因为你通常会添加断言和模拟库 |
| 断言 | 内置 | 通常与另一个库配对 |
| 模拟 | 内置 | 通常与另一个库一起使用 |
| 异步测试 | 内置且直接 | 支持,但依赖于周围的设置 |
| 覆盖工作流 | 常常集成到同一个工具链中 | 经常是拼凑起来的 |
| 最佳选择 | 新项目,希望保持一致性的团队 | 旧堆栈,希望有模块化控制的团队 |
实用规则: 如果您的团队需要询问哪个断言库和模拟库与运行器配对时,您可能希望使用 Jest。
对于大多数团队的建议
对于大多数现代项目,我建议 Jest 除非代码库已经有强烈的理由继续使用 Mocha。 Capacitor 或 Electron,因为这些项目已经有足够的移动部分。减少测试工具的杂乱程度会带来快速的收益。
Mocha 在较旧的 Node.js 服务或长期存活的代码库中仍然有意义,因为其周围的生态系统已经稳定。但是,对于一名中级工程师从头开始设置一个强大的测试套件时,Jest 通常比它创建的摩擦要少。
一个重要的范围说明。Cypress 和 Playwright 是优秀的工具,但它们解决的是不同的问题。它们更适合于浏览器级别和端到端检查,而不是 JavaScript 工作应该居住的快速内部循环中进行的单元测试。
项目设置和您的第一个测试
A clean testing setup should be boring. If adding the first test feels complicated, the suite probably won’t stay healthy.

一个简单的 Jest 设置
从一个已经有一个 package.json的 JavaScript 项目开始。然后将 Jest 添加为开发依赖项,并配置一个测试脚本。
{
"scripts": {
"test": "jest"
}
}
对于许多项目来说,这已经足够了。如果你的模块系统、转译或多包结构需要配置更多的选项,可以在之后添加。
如果你正在本地构建一个 Capacitor 应用,并且想要在添加测试到共享逻辑之前,将开发环境保持有序,Capgo 的指南关于 设置一个 Capacitor 本地环境 是一个实用的伴侣。
在写 code 之前,先写一个测试
测试优先模式不是个人偏好。美国消费者金融保护局的 JavaScript 指南明确推荐 在写 __CAPGO_KEEP_0__ 之前先写一个测试因为测试驱动开发改变了你设计__CAPGO_KEEP_0__的方式,测试驱动开发的变化会影响到函数的大小、依赖的可见性以及副作用的泄露。 describe 测试驱动开发的好处在于它改变了你设计__CAPGO_KEEP_0__的方式,测试驱动开发的变化会影响到函数的大小、依赖的可见性以及副作用的泄露。 it测试驱动开发的好处在于它改变了你设计__CAPGO_KEEP_0__的方式,测试驱动开发的变化会影响到函数的大小、依赖的可见性以及副作用的泄露。 expect(...) 测试驱动开发的好处在于它改变了你设计__CAPGO_KEEP_0__的方式,测试驱动开发的变化会影响到函数的大小、依赖的可见性以及副作用的泄露。 测试驱动开发的好处在于它改变了你设计__CAPGO_KEEP_0__的方式,测试驱动开发的变化会影响到函数的大小、依赖的可见性以及副作用的泄露。.
测试驱动开发的好处在于它改变了你设计code的方式,测试驱动开发的变化会影响到函数的大小、依赖的可见性以及副作用的泄露。
测试驱动开发的好处在于它改变了你设计__CAPGO_KEEP_0__的方式,测试驱动开发的变化会影响到函数的大小、依赖的可见性以及副作用的泄露。
// 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);
});
});
测试驱动开发的好处在于它改变了你设计__CAPGO_KEEP_0__的方式,测试驱动开发的变化会影响到函数的大小、依赖的可见性以及副作用的泄露。
测试驱动开发的好处在于它改变了你设计__CAPGO_KEEP_0__的方式,测试驱动开发的变化会影响到函数的大小、依赖的可见性以及副作用的泄露。 测试驱动开发的好处在于它改变了你设计__CAPGO_KEEP_0__的方式,测试驱动开发的变化会影响到函数的大小、依赖的可见性以及副作用的泄露。 测试驱动开发的好处在于它改变了你设计__CAPGO_KEEP_0__的方式,测试驱动开发的变化会影响到函数的大小、依赖的可见性以及副作用的泄露。
- 测试驱动开发的好处在于它改变了你设计__CAPGO_KEEP_0__的方式,测试驱动开发的变化会影响到函数的大小、依赖的可见性以及副作用的泄露。 输入和所需的设置。
- 动作 通过调用函数
- 断言 对结果
应用于验证辅助函数:
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项目来说,遵守这一规则更为重要,因为您的纯逻辑通常与native或桌面集成放在一起code。在不依赖平台运行时的情况下,保持业务规则可测试,这样您的第一个测试就不会是最后一个有用的测试。
掌握Mock和异步Code
大多数应用code中的错误并不是来自于两个数字的加法。它们来自于code:网络请求、文件、插件API、定时器、IPC通道、存储层等。
这就是Mock的作用。它让您对边界有控制权,使测试可以专注于code的决策过程。

模拟边界,不是所有的
可维护的测试指导强调 单一行为覆盖 和 每个测试一个强大的断言,它还警告说过度使用模拟会使测试脆弱并紧密耦合到实现细节中,如本 TestRail文章关于可维护的单元测试.
这警告在JavaScript中非常重要。团队经常从模拟每个导入的模块开始,最后测试函数是否正确调用其他函数,而不是测试真实行为。
模拟过多的测试目标不佳:
- 是否helper A调用了helper B
- 是否服务C调用了序列化器D
- 是否内部私有函数运行了两次
更好的目标:
- 函数返回的内容是什么
- 它是否正确处理了依赖项失败
- 它是否将数据转换为期望的形状
更好的模式:Capacitor和Electroncode
在移动和桌面应用中,我更喜欢在原生或平台API周围包装一个wrapper层。然后,单元测试模拟wrapper,而不是平台本身。
示例结构:
// 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。.wrap ipcRenderer文件访问、shell集成等后面都有一个薄的适配器。单元测试击中服务层,而不是直接击中运行时。
对于测试Capacitor应用的发布逻辑和更新路径的团队,Capgo有一个相关的指南关于 测试Capacitor OTA更新的模拟场景.
快速的教程可以帮助您的团队正常化异步测试风格:
异步流程测试
使用 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');
});
测试两条路径:happy path和ugly path。在生产环境中,ugly path往往是用户记住的那一条。
构建可靠软件的高级策略
测试套件变得有用,当它在code发生变化后仍然有用时。这比写一堆通过的测试更难。

将测试分成预算
一项实用的指南建议将测试分成 70/20/10 单元测试、集成测试和端到端测试 测试分割预算与单元测试提供最快的反馈和最稳定的故障。同样的指导建议,一个完整的单元测试套件应该理想情况下在 10 秒内完成,并且预提交检查应该保持在 5 秒内,根据这个 OpenReplay 测试指南.
我把它当作一个预算工具,而不是一种宗教。如果大部分你的努力都花在了端到端测试上,你的团队会等待太长时间才能获得反馈。如果一切都是单元测试,你会错过真正的系统边界。
对于一个 Capacitor 或 Electron 应用程序,一个健康的平衡通常如下:
- 单元测试 用于价格逻辑、权限规则、序列化、更新资格、特性标志和状态转换
- 集成测试 用于存储适配器、插件包装器和 IPC 协议
- E2E测试 为关键旅程,如登录、购买流程、同步或更新提示,编写几个关键用例
覆盖率是一个手电筒,而不是目标
覆盖率报告在帮助您发现重要逻辑中未测试的 branch 时是有用的。然而,当团队为了自己的利益而追求覆盖率百分比时,它们就变成了有害的
一个认真考虑边缘案例的登录验证器比一个包含大量无关紧要断言的覆盖的文件更有价值。尤其是对于输入密集的code,如表单、解析器、日期逻辑和权限检查。如果您的团队正在围绕验证密集的UI提高质量,这篇关于 前端表单验证的指南 是与单元测试策略相结合的好补充
行为驱动测试可以抵抗重构
一个可靠的测试套件应该让您能够重构内部逻辑而不必重写一半的测试。实现这一点的最简单方法是断言 可观察行为 而不是实现细节
适用场景:
- 边界条件 例如空输入、空值、无效类型和过长字符串
- 域结果 例如“缺少权限时拒绝返回”
- 状态转换 例如“下载元数据验证后标记更新为待处理”
常常会过时的用例
- 例如检查内部辅助函数调用
- 例如断言私有方法顺序
- 例如模拟整个调用链
对于构建有条不紊发布流程的应用团队,Capgo的文章 应用质量保证 因为它将测试工作与更广泛的发布管道连接起来,所以它很有用。
CI、Capacitor 和 Electron 应用程序的测试
一个只在开发者机器上运行的测试并不是一个安全网。它是一个局部习惯。
CI 将单元测试的 JavaScript 工作转化为团队基础设施。每次推送、拉取请求或发布分支都可以使用相同的命令和相同的期望来执行相同的命令。这种一致性对于 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
这足以在主分支之前捕获错误的导入、失败的断言和意外的平台假设。
通过自动化管道交付移动应用的团队,Capgo 提供了一个实用的指南来 为 Capacitor 应用程序设置 CI/CD.
测试 Capacitor 插件交互
错误的单元测试 Capacitor code 的方式是将本机插件直接拉入每个服务。这样会将测试套件与平台桥接耦合起来。
The better pattern is a thin abstraction:
// 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 });
});
同样的想法也适用于摄像头访问、生物识别提示、推送令牌注册和网络状态。将插件调用放在适配器中。测试应用逻辑以控制的接口为目标。
测试 Electron 主进程和 IPC code
Electron 应用程序有两个重要的缝隙: 主进程 code 和 渲染进程 code. 不要在测试中混淆它们。
可靠的设置通常分离为:
- 渲染单元测试 用于视图模型、状态、格式化和 UI 端业务逻辑
- 主进程单元测试 for 菜单、文件操作和应用程序生命周期决策
- IPC 合约测试 for 消息形状和预期响应
示例 IPC wrapper:
// 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' });
});
如果您稍后将内部实现从一个助手更改为另一个助手,这个测试仍然有效,因为它验证了重要的行为。 That’s 桌面和移动设备上的标准您希望看到 code。
关于 JavaScript 单元测试的常见问题
单元测试和 E2E 测试之间的区别是什么
A 单元测试 检查一个小的逻辑单元的隔离。一个 集成测试 检查几个组件或服务是否正确地一起工作。一个 end-to-end 测试 end-to-end 测试会模拟用户在运行中的应用程序的整个流程。
使用单元测试来快速确保业务规则。使用集成测试来测试存储、插件包装器和IPC等接口。使用E2E测试谨慎地测试那些如果它们发生故障会严重影响的工作流程。
我们应该追求全面覆盖
不。全面覆盖可能会迫使团队向低价值测试靠拢。
覆盖率在它揭示了没有被执行的风险code时是有用的。它在工程师们只是为了满足仪表板而添加浅表断言时并不是有用的。如果您的测试套件脆弱,更多的覆盖率也不会救它。
如何在现有代码库中添加测试
从变化已经发生的地方开始。不要冻结团队并宣布巨大的测试策略重写。
一个实际的顺序如下:
- 首先保护活跃code 通过在功能工作或bug修复中添加测试到您接触的模块
- 提取纯粹的逻辑 从难以测试的文件中抽取业务规则,以便在不引入框架或运行时噪音的情况下测试它们
- 在native插件、网络客户端、文件系统调用和Electron IPC周围添加接口 拒绝易碎模式
- 当引入模拟时 JavaScript测试最佳实践的指导 特别有用,因为它突出了过度模拟和随之而来的易碎测试的常见问题 目标不是立即完成。它是团队在最容易导致回归的领域稳步改进
如果您的团队交付
__CAPGO_KEEP_0__ Capacitor Electron If your team ships __CAPGO_KEEP_0__ or Electron apps 和需要一个更清洁的发布过程来处理 JavaScript 变化, Capgo 是查看的一个选项。它为 CapacitorJS 和 Electron 应用程序提供实时更新,具有回滚控制和可观察性,团队可以将坚实的单元测试与更安全的路径配对,来发布 web 包更改,而不必等待每个修复的商店审查。