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

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

一个简单的Jest设置
从一个已经有一个的JavaScript项目开始。然后添加Jest作为开发依赖项,并连接一个测试脚本。 package.json这对于许多项目来说已经足够了。您可以在您的模块系统、转译、或多包结构需要时添加更多配置。
{
"scripts": {
"test": "jest"
}
}
__CAPGO_KEEP_0__
如果您正在本地构建一个Capacitor应用,并希望在添加共享逻辑的测试之前将开发环境保持有序,Capgo关于 设置Capacitor本地环境 是一个实用的指南。
在code之前编写测试
测试优先模式不仅仅是个人偏好。美国消费者金融保护局的JavaScript指南明确推荐 在其JavaScript单元测试指南中编写测试 describe 组织测试 it并以 expect(...) 断言 在其JavaScript单元测试指南中.
这很重要,因为测试优先会改变您设计code的方式。函数倾向于变小,依赖项变得更加可见,副作用停止渗透到应该保持纯粹的逻辑中。
Here’s a minimal example:
// 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
The Arrange, Act, Assert 模式使测试保持可读性,即使它们变得更加复杂。
- Arrange 准备输入和任何必要的设置。
- Act 通过调用函数。
- Assert 在结果上。
应用于验证辅助函数:
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放在一起。保持业务规则在不使用平台运行时的情况下可测试,第一条测试不会是最后有用的测试。
掌握Code和异步的技巧
应用程序code中的大多数bug并不是来自于添加两个数字。它们来自于code:网络请求、文件、插件API、定时器、IPC通道、存储层。
这就是mocking的作用。它让您控制边界,使测试可以专注于code的决策过程。

不要模拟所有东西,模拟边界
可维护的测试指南强调 单一行为覆盖 并且 每个测试只有一条强大的断言并且它也警告说过度使用mock会使测试变得脆弱并且紧密耦合到实现细节中,如本文所总结的 简化的单元测试文章.
在JavaScript中,那个警告很重要。团队经常从模拟所有导入的模块开始,最后却测试函数是否正确调用了其他函数,而不是测试真实的行为。
不适合使用大量模拟的测试目标:
- helper A是否调用了helper B
- service C是否调用了serializer D
- 内部私有函数是否运行了两次
更好的目标:
- 函数返回了什么
- 它是否正确处理了依赖项的失败
- 它是否将数据转换成了期望的形状
A better pattern for Capacitor and Electron code
在移动和桌面应用中,我更喜欢在native或platform API周围创建一个wrapper层。然后,单元测试模拟wrapper,而不是平台本身。
Example structure:
// 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');
});
测试两条路径:成功路径和失败路径。在生产环境中,失败路径往往是用户记住的那一条。
高级策略:构建健壮的测试
当测试套件在code变化后仍然有用时,它才会变得有用。写一堆通过的测试比这更难。

将测试分离作为预算
一本实用的指南建议将测试分成 70/20/10 单元测试、集成测试和端到端测试 ,其中单元测试提供最快的反馈和最稳定的故障。同样的指导建议,一个完整的单元测试套件应该在10秒钟内 完成,并且预提交检查应该在5秒钟内 ,根据OpenReplay测试指南, according to this OpenReplay testing guide.
I把它当作一个预算工具,而不是一种宗教。如果大部分的努力都花在了端到端测试上,那么你的团队会等待太久才能获得反馈。如果所有的测试都是单元测试,你会错过真正的系统边界。
对于一个Capacitor或Electron应用程序,一种健康的平衡通常是这样的:
- 单元测试 用于价格逻辑、权限规则、序列化、更新资格、功能标志和状态转换
- 集成测试 用于存储适配器、插件包装器和IPC协议
- 端到端测试 用于几个关键的旅程,如登录、购买流程、同步或更新提示
覆盖率是一个手电筒,而不是一个目标
覆盖率报告在帮助你-spot未测试的branch在重要逻辑时是有用的。当团队追求覆盖率百分比时,覆盖率报告就变成了有害的。
一个对边界情况进行了深思熟虑的登录验证器比一个包含了大量无关紧要断言的覆盖的文件更有价值。尤其是对于输入密集的code,如表单、解析器、日期逻辑和权限检查。如果你的团队正在围绕验证密集的UI提高质量,这个关于 掌握前端表单验证的指南 是单元测试策略的良好补充。
行为驱动测试可以抵御重构
可靠的测试集应该允许您重构内部实现而不必重写大量测试。让您轻松实现这一点的方法是断言 可观察行为 而不是实现细节。
能经得住考验的用例包括
- 边界条件 例如空输入、空值、无效类型和过长字符串
- 域结果 例如“缺少权限时拒绝返回”
- 状态转换 例如“下载元数据验证后标记更新为待处理”
常见的过期用例:
- 内部辅助函数调用
- 私有方法顺序
- 模拟调用链中的每层
For app teams building disciplined release processes, Capgo’s article on 应用质量保证 有用,因为它将测试工作与更广泛的发布管道联系起来。
针对CI、Capacitor和Electron应用的测试
一个只在一个开发者的机器上运行的测试并不是一个安全网。它是一个局部习惯。
CI将单元测试的JavaScript工作转化为团队基础设施。每次推送、拉取请求或发布分支都可以执行相同的命令并且具有相同的期望。这种一致性对于Capacitor和Electron项目来说尤其重要,因为环境漂移会导致微小的故障。
将CI设置为默认执行路径
至少,您的CI应该在每次变更集上安装依赖项并运行单元测试套件。尽可能保持命令与本地开发时相同。
A basic 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。这会将测试套件耦合到平台桥接上。
更好的模式是使用薄的抽象:
// 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 端业务逻辑
- 主进程单元测试 用于菜单、文件操作和应用程序生命周期决策
- IPC 契约测试 用于消息形状和预期响应
示例 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' });
});
如果您稍后将内部实现从一个助手更改为另一个助手,这个测试仍然有效,因为它验证了重要的行为。 这就是您希望在桌面和移动设备上实现的标准 code。
关于 JavaScript 单元测试的常见问题
What’s the difference between unit integration and E2E tests
A 单元测试 检查一个小的逻辑单元是否正确。 集成测试 检查几个组件或服务是否能正确工作。 端到端测试 模拟用户在运行的应用中进行的旅程。
使用单元测试快速确认商业规则。使用集成测试检查存储、插件包装器和IPC等缝隙。使用E2E测试谨慎地检查那些如果它们破坏了会严重影响的工作流程。
是否应该追求全面覆盖
不。全面覆盖可能会导致团队向低价值测试倾斜。
覆盖率在揭示没有被执行的风险code时有用。它在工程师添加浅表断言仅满足仪表板时没有用处。如果您的测试套件脆弱,更多的覆盖率也不会救它。
如何在现有代码库中添加测试
从变化的地方开始。不要冻结团队并宣布测试策略的大规模重写。
一个实际的顺序如下:
- 首先保护活跃的code 通过在特性工作或bug修复中添加测试来保护活跃的__CAPGO_KEEP_0__
- 从难以测试的文件中提取纯粹的逻辑 以便在不受框架或运行时噪音影响的情况下测试商业规则
- 在原生插件、网络客户端、文件系统调用和Electron IPC周围添加接口 拒绝引入模拟的脆弱模式
- 从 JavaScript测试最佳实践指南 拒绝引入模拟的脆弱模式 在这里尤其有用,因为它突出了过度模拟和随之而来的脆弱测试的常常被忽视的问题
目标不是立即完成,而是稳步改进在团队中最昂贵的回归问题的位置
如果您的团队交付 Capacitor 或 Electron 应用并需要更干净的JavaScript变化的发布过程 Capgo 是团队可以将坚实的单元测试与不必等待每个修复的商店审查的更安全的路径配对的选项