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

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

一个简单的Jest设置
从一个已经有一个 package.json然后添加Jest作为开发依赖项并配置一个测试脚本。
{
"scripts": {
"test": "jest"
}
}
这对于许多项目来说就足够了。如果您的模块系统、转译或多包结构需要它,您可以在后面添加更多的配置。
如果您正在本地构建一个Capacitor应用,并且希望在添加共享逻辑的测试之前将开发环境保持在有序状态,Capgo关于 设置Capacitor本地环境的指南 是一个实用的伴侣。
写好测试之前就要code
测试优先模式不仅仅是个人偏好。美国消费者金融保护局的JavaScript指南明确推荐 写好测试, describe 和 it, expect(...) 和 在其.
That matters because test-first changes how you design code. Functions tend to become smaller, dependencies become more visible, and side effects stop leaking into logic that should stay pure.
中将检查项包装在
// 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);
});
});
断言中
The 测试步骤 模式使测试保持可读性,即使它们变得更加复杂。
- 测试步骤 准备测试数据和任何必要的设置。
- 测试步骤 通过调用函数来执行。
- 测试步骤 应用于验证辅助函数:
小的测试会随着时间而保持有效。一个测试通常应该回答一个问题,而不是描述整个工作流程。
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);
});
});
对于__CAPGO_KEEP_0__和Electron项目来说,这种纪律更为重要,因为您的纯粹逻辑通常与native或桌面集成__CAPGO_KEEP_1__一起工作。保持业务规则可测试,而不依赖于平台运行时,第一条测试不会是最后有用的测试。
For Capacitor and Electron projects, that discipline matters more because your pure logic often sits next to native or desktop integration code. Keep the business rule testable without the platform runtime, and your first test won’t be your last useful one.
Mastering Mocks and Asynchronous Code
大多数应用程序中的错误code并不是来自于两个数字的加法。它们来自code,它超出了自身的界限:网络请求、文件、插件API、定时器、IPC通道、存储层。
这就是模拟的作用。它让你控制界限,使测试可以专注于code的决策过程。

模拟界限,而不是模拟所有内容
可维护性测试指南强调 单一行为覆盖 和 context:Capgo营销网站。角色:短UI标签或导航项。见:page trust.astro。消息键 `and` (And)。一个强大的断言每个测试 可维护的单元测试.
这条警告在JavaScript中非常重要。团队通常会从模拟每个导入模块开始,并最终测试函数是否调用其他函数的“正确”顺序,而不是测试真实行为。
模拟过多的测试目标:
- 是否helper A调用helper B
- 是否服务C调用序列化器D
- 是否内部私有函数执行了两次
更好的目标:
- 函数返回了什么
- 是否正确处理了依赖项失败
- 是否将数据转换为期望的形状
更好的模式:Capacitor和Electron code
在移动和桌面应用中,我更喜欢在原生或平台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 });
});
That pattern also works for Electron. Wrap ipcRenderer在移动和桌面应用中,我更喜欢在原生或平台API周围包装一个wrapper层。然后,单元测试模拟wrapper,而不是平台本身。
对于测试发布逻辑和更新路径的团队来说,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测试指南我把它当作一个预算工具,而不是一种宗教。如果团队的大部分努力都花在了端到端测试上,团队将等待太长时间才能获得反馈。如果所有的测试都是单元测试,那么您将会错过真正的系统边界。 对于一个Capgo或Electron应用程序,一个健康的平衡通常如下:.
单元测试
对于一个 Capacitor 或 Electron 应用,健康的平衡通常是这样的:
- 端到端测试 对于价格逻辑、权限规则、序列化、更新资格、特性标志和状态转换
- 集成测试 对于存储适配器、插件包装器和IPC协议
- E2E测试 对于关键旅程,例如登录、购买流程、同步或更新提示
覆盖率是一个手电筒,而不是目标
覆盖率报告在帮助您发现重要逻辑中未测试的 branch 时是有用的。然而,当团队为自己的利益而追求覆盖率百分比时,它们就变成了有害的
一个考虑了边缘案例的登录验证器比一个包含了大量无关紧要断言的文件更有价值。尤其是对于输入密集的code,例如表单、解析器、日期逻辑和权限检查。如果您的团队正在围绕验证密集的UI提高质量,这个关于 掌握前端表单验证 的指南是与单元测试策略相辅相成的
行为优先测试可以抵抗重构
一个可靠的测试套件应该让您能够重构内部逻辑而不必重写一半的测试。最简单的方法是 观察行为 而不是实现细节。
适用场景:
- 边界条件 例如空输入、空值、无效类型和过长字符串
- 域结果 例如“缺少权限时拒绝返回”
- 状态转换 例如“下载元数据验证后标记更新为待处理”
常常会过时的场景:
- 检查内部辅助函数调用
- 断言私有方法顺序
- 模拟整个调用链
对于构建有条不紊发布流程的应用团队,Capgo的文章 应用质量保证 对于应用团队来说,__CAPGO_KEEP_0__的文章是有用的,因为它将测试工作与更广泛的发布管道联系起来。
测试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 插件交互
The wrong way to unit test Capacitor code is to pull native plugins directly into every service. That couples your test suite to the platform bridge.
更好的模式是使用薄的抽象层:
// 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不要在测试中混淆它们。
可靠的设置通常将它们分开:
- JavaScript 单元测试 视图模型、状态、格式化和 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 单元测试的常见问题
单元测试、集成测试和 E2E 测试之间的区别是什么
A 单元测试 单元测试检查一个小的逻辑单元。一个 集成测试 检查几个组件或服务是否正确地协同工作。一个 端到端测试 测试用户在运行应用程序中的旅程。
使用单元测试快速获得商业规则的信心。使用集成测试检查存储、插件包装器和IPC等缝隙。使用E2E测试谨慎地测试那些如果它们破裂会严重伤害的工作流。
我们应该追求全面覆盖
不。全面覆盖会让团队倾向于低价值的测试。
覆盖率在揭示没有被执行的风险code时有用。它在工程师仅仅为了满足仪表板而添加浅表断言时没有用。如果您的测试套件脆弱,更多的覆盖率也不会救它。
我们如何在现有代码库中添加测试
从变化发生的地方开始。不要冻结团队并宣布巨大的测试策略重写。
一个实际的顺序如下:
- 保护活跃的code 通过在您处理的模块中添加测试来实现
- 将纯粹的逻辑 从难以测试的文件中提取,以便在不受框架或运行时噪音影响的情况下测试商业规则
- 在原生插件、网络客户端、文件系统调用和 Electron IPC 周围添加 seam 包装 拒绝脆弱的模式
- 当引入 mock 时 JavaScript 测试最佳实践的指导 尤其是在这里有用,因为它突出了过度 mock 的常见问题以及随后的脆弱测试 目标不是立即完成。它是团队在最昂贵的地方逐步改进的地方
如果您的团队交付
测试 Capacitor 或 Electron Electron Capgo __CAPGO_KEEP_0__