You’re probably in one of two situations right now. Either your JavaScript project has almost no tests and every refactor feels risky, or you already have tests and half of them are slow, brittle, and oddly hard to trust.
That gets worse in Capacitor and Electron apps. A simple feature can touch shared business logic, browser APIs, native plugins, local files, IPC, and remote services in the same flow. If you test those pieces the wrong way, your suite becomes a maze of fake dependencies. If you test them the right way, you get fast feedback on the logic that breaks.
Good unit tests JavaScript work doesn’t start with clever matcher syntax. It starts with a disciplined boundary: test pure logic directly, isolate side effects, and avoid writing tests that collapse the moment you rename an internal function.
目录
- 选择您的 JavaScript 测试框架
- 项目设置和您的第一个测试
- 掌握模拟和异步 Code
- 高级策略以确保测试的可靠性
- CI、Capacitor和Electron应用的测试
- 关于JavaScript单元测试的常见问题
选择JavaScript测试框架
一个专业的JavaScript项目需要一个真正的测试运行器。临时脚本和手动控制台检查在多个工程师处理同一代码库时无法扩展。您需要测试发现、断言、异步处理、模拟和在本地开发和CI中一致地运行所有内容的方式。
当前指导方针正在趋向于小范围的主流选项。 Jest、Mocha和Jasmine 经常被突出显示为主要框架, __CAPGO_KEEP_0__ 常常因为内置的测试结构、断言、模拟和异步支持而被单独提及, 如下Pluralsight JavaScript测试实验室.

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

一个简单的 Jest 设置
从一个已经有一个的 JavaScript 项目开始。然后添加 Jest 作为开发依赖项并配置一个测试脚本。 package.json这对于许多项目来说已经足够了。您可以在需要时根据模块系统、转译或多包结构添加更多配置。
{
"scripts": {
"test": "jest"
}
}
如果您正在本地构建一个 __CAPGO_KEEP_0__ 应用,并且希望在添加测试之前将开发环境保持在有序状态,__CAPGO_KEEP_1__ 的指南关于
If you’re building a Capacitor app locally and want your dev environment in order before adding tests around shared logic, Capgo’s guide to setting up a Capacitor local environment 在 __CAPGO_KEEP_0__ 之前编写测试。
code
The test-first pattern 不仅仅是一种个人偏好。美国消费者金融保护局的 JavaScript 指南明确推荐 写测试之前组织测试 describe 和 it围绕 expect(...) 在其 JavaScript 单元测试指南.
这很重要,因为 test-first 会改变您设计 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
The Arrange, Act, Assert 模式保持测试可读性,即使它们变得更加复杂。
- 准备 执行
- 验证 应用于验证辅助函数:
- 小测试耐久。一个测试通常应该回答一个问题,而不是叙述整个工作流程。 对于__CAPGO_KEEP_0__和Electron项目来说,这种纪律更为重要,因为您的纯粹逻辑通常与native或桌面集成__CAPGO_KEEP_1__一起工作。保持业务规则可测试,而不依赖于平台运行时,第一条测试不会是最后有用的测试。
掌握模拟和异步__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);
});
});
大多数应用__CAPGO_KEEP_0__中的错误并不是来自于添加两个数字。它们来自__CAPGO_KEEP_1__,它超出了自身:网络请求、文件、插件API、定时器、IPC通道、存储层
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
Most bugs in application code don’t come from adding two numbers. They come from code that reaches outside itself: network requests, files, plugin APIs, timers, IPC channels, storage layers.
That’s where mocking helps. It gives you control over the boundary so the test can focus on your code’s decision-making.

不要模拟所有内容
可维护性测试指南强调 单一行为覆盖 和 每个测试只有一条强大的断言, 并且它也警告说过度使用模拟会使测试变得脆弱并且紧密耦合到实现细节中,如本 TestRail文章关于可维护单元测试.
这个警告在JavaScript中非常重要。团队经常从模拟所有导入模块开始,并最终测试函数是否正确调用其他函数,而不是测试真实行为。
模拟过多的测试目标:
- 是否函数A调用函数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 });
});
这个模式也适用于 Electron。Wrap ipcRenderer文件访问、shell 集成等在一个薄的适配器后面。单元测试击中服务层,而不是直接运行时。
对于测试发布逻辑和更新路径的Capacitor应用的团队,Capgo有一个相关的指南 测试 Capacitor OTA 更新与模拟场景.
如果您的团队仍在正常化异步测试风格,快速的教程可能会有所帮助:
在测试异步流程时避免不稳定
使用 async/await 在测试中,当被测试的 code 返回一个 Promise 时使用。它比 callback-heavy 模式更清晰,并且更容易调试。
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 测试指南.
我把它当作一个预算工具,而不是一种宗教。如果你的团队花费太多时间在端到端测试上,反馈会太慢。如果只进行单元测试,你会错过系统边界。
对于一个 Capacitor 或 Electron 应用程序,一个健康的平衡通常是这样的:
- 单元测试 用于价格逻辑、权限规则、序列化、更新资格、特性标志和状态转换
- 集成测试 用于存储适配器、插件包装器和IPC协议的测试
- E2E测试 用于关键旅程,如登录、购买流程、同步或更新提示的测试
覆盖率是一个手电筒,而不是目标
覆盖率报告在帮助您发现重要逻辑中未测试的 branch 时是有用的。然而,当团队为了自己的利益而追求覆盖率百分比时,它们就变成了有害的。
一个考虑了边缘案例的登录验证器比一个包含了大量无关紧要断言的覆盖的文件更有价值。尤其是对于输入密集的code,例如表单、解析器、日期逻辑和权限检查。如果您的团队正在围绕验证密集的UI提高质量,这篇关于 前端表单验证的指南 的指南是单元测试策略的良好补充。
行为优先的测试可以抵抗重构
一个可靠的测试套件应该让您能够重构内部逻辑而不必重写一半的测试。让您轻松实现这一点的最简单方法是断言 可观察行为 而不是实现细节。
能经得住考验的使用场景:
- 边界条件 如空输入、空值、无效类型和过长字符串
- 领域结果 如“缺少权限时拒绝返回”
- 状态转换 如“下载元数据验证后标记更新为待处理”
经常会变质的使用场景:
- 检查内部辅助函数调用
- 断言私有方法顺序
- 模拟整个调用链的每层
For app teams building disciplined release processes, Capgo’s article on 应用质量保证 是有用的,因为它将测试工作与更广泛的发布管道联系起来。
CI、Capacitor 和 Electron 应用测试
仅在开发者机器上运行的测试并不是一个安全网。它是一个局部习惯。
CI 将单元测试的 JavaScript 工作转化为团队基础设施。每次推送、拉取请求或发布 branch 都可以执行相同的命令并且具有相同的期望。这种一致性对于 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。这使得测试套件与平台桥接紧密耦合。
更好的模式是使用薄的抽象:
// 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 单元测试的常见问题
单元测试和 E2E 测试之间的区别是什么
A 单元测试 检查一个小的逻辑单元的隔离状态。 integration test 检查几个组件或服务是否能正确协同工作。 end-to-end test 模拟用户在运行中的应用程序进行一系列操作.
使用单元测试来快速确认业务规则。使用集成测试来检查存储、插件包装器和IPC等 seam。使用E2E测试谨慎地检查那些如果它们出现问题会严重影响的工作流程。
我们应该追求全面覆盖
不。全面覆盖可能会让团队倾向于编写低价值的测试。
覆盖率在揭示没有被执行的风险code时是有用的。它在工程师为了满足仪表板而添加浅表断言时并不是有用的。如果您的测试套件脆弱,更多的覆盖率也不会救它。
如何在现有代码库中添加测试
从变化已经发生的地方开始。不要冻结团队并宣布巨大的测试策略重写。
一个实际的顺序如下:
- 首先保护正在活动的code 通过在功能工作或bug修复期间添加对模块的测试来实现这一点
- 提取纯粹的逻辑 从难以测试的文件中提取业务规则,以便在不受框架或运行时噪音影响的情况下测试
- 在原生插件、网络客户端、文件系统调用和 Electron IPC 周围添加 seam 包装 拒绝脆弱的模式
- 当引入 mock 时 JavaScript 测试最佳实践的指导 尤其是在这里有用,因为它突出了过度 mock 的常见问题以及随后的脆弱测试 目标不是立即完成。它是团队在最容易导致团队损失的地方稳步改进的地方
如果您的团队交付
__CAPGO_KEEP_0__ Capacitor 或 Electron 需要更清晰的 JavaScript 变更的发布流程, Capgo 是您可以考虑的选项之一。它为 CapacitorJS 和 Electron 应用程序提供实时更新,具有滚动控制和可观察性,使团队能够将坚实的单元测试与不必等待每个修复的商店审查的更安全的路径结合起来,来发布 Web 包变更。