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

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

一个简单的 Jest 设置
从一个已经有一个 package.json的 JavaScript 项目开始。然后将 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 Arrange, 行动, 断言 这种模式使测试保持可读性,即使它们变得更加复杂。
- 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 项目,这种纪律更为重要,因为您的纯逻辑通常与本机或桌面集成 code 相邻。保持业务规则可测试,而不依赖于平台运行时,第一个测试将不会是最后一个有用的测试。
掌握模拟和异步Code
应用code中最常见的bug不是来自于两个数字的加法。它们来自code:网络请求、文件、插件API、定时器、IPC通道、存储层。
模拟帮助你控制边界,让测试可以专注于code的决策过程。

不要模拟所有东西
可维护性测试指南强调 单一行为覆盖 和 每个测试只有一条强大的断言, ,并且它也警告说过度使用模拟会使测试变得脆弱并且紧密耦合到实现细节,正如本.
TestRail文章关于可维护单元测试的总结中所述。
不适合用来模拟测试的目标:
- 助手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。将原生API包裹在wrapper层中 ipcRenderer通过一个薄的适配器,实现文件访问或shell集成。单元测试直接击中服务层,而不是直接击中运行时。
对于测试发布逻辑和更新路径的Capacitor应用团队,Capgo有一个相关的指南。 测试CapacitorOTA更新的模拟场景.
快速的教程可以帮助您的团队正常化异步测试风格:
测试异步流程而不出现不稳定性
在测试中使用 async/await 当code测试项返回一个Promise时使用。比起callback模式更清晰,易于调试。
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 单元测试、集成测试和端到端测试 ,其中单元测试提供最快的反馈和最稳定的故障。同样的指导建议,一个完整的单元测试套件应该在秒内完成 ,而预提交检查应该保持在秒内 ,根据这个OpenReplay测试指南 我把它当作一个预算工具,而不是一种宗教。如果大部分你的努力都花在了端到端测试上,你的团队会等待太长时间才能获得反馈。如果一切都是单元测试,你会错过真正的系统边界。.
对于一个
For a Capacitor or Electron app, a healthy balance usually looks like this:
- 单元测试 用于价格逻辑、权限规则、序列化、更新资格、特性标志和状态转换
- 集成测试 用于存储适配器、插件包装器和IPC协议
- 端到端测试 用于几个关键旅程,如登录、购买流程、同步或更新提示
覆盖率是一个手电筒,而不是目标
覆盖率报告在帮助您-spot未测试的 branch 在重要逻辑时是有用的。它们变得有害,当团队追求覆盖率百分比时
一个认真考虑边缘案例的登录验证器比一个覆盖了大量无关紧要断言的文件更有价值。尤其是对于输入密集的code,如表单、解析器、日期逻辑和权限检查。如果您的团队正在围绕验证密集的UI提高质量,这个关于 前端表单验证的指南 是单元级测试策略的良好补充
行为优先测试可以抵抗重构
一个可靠的套件应该允许您重构内部实现而不重写测试的一半。让您轻松实现这一点的方法是断言 可观察行为 而不是实现细节。
能经得起考验的用例:
- 边界条件 如空输入、null类值、无效类型和过长字符串
- 域结果 如“拒绝返回由于缺少权限”
- 状态转换 如“标记更新为待处理状态后下载元数据验证”
经常会腐烂的用例:
- 检查内部辅助函数调用
- 断言私有方法顺序
- 模拟调用链中的每层
对于构建有纪律发布流程的应用团队,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。这会将测试套件耦合到平台桥接中
更好的模式是使用薄的抽象:
// 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 单元测试 检查一个小的逻辑单元。一个 集成测试 检查几个组件或服务是否正确地协同工作。一个 端到端测试 通过运行应用程序来模拟用户旅程。
使用单元测试快速获得商业规则的信心。使用集成测试检查存储、插件包装器和IPC等缝隙。使用E2E测试谨慎地检查那些如果它们破坏了会严重影响的工作流。
我们应该追求全面覆盖
不。全面覆盖可以推动团队朝着低价值测试的方向发展。
覆盖率在它揭示了没有人尝试过的风险code时是有用的。它不是有用的,当工程师仅仅为了满足仪表板而添加浅表断言时。
我们如何在现有代码库中添加测试
从变化已经发生的地方开始。不要冻结团队并宣布巨大的测试策略重写。
实践序列的例子如下:
- 首先保护活动的code 通过在您处理的功能或 bug 修复期间添加测试来保护活动__CAPGO_KEEP_0__
- 从难以测试的文件中提取纯逻辑 以便在不受框架或运行时噪音影响的情况下测试商业规则
- 在原生插件、网络客户端、文件系统调用和 Electron IPC 周围添加 seam 包装 拒绝脆弱的模式
- 当引入 mock 时,特别是因为它突出了人们经常忽视的过度 mock 和随后的脆弱测试问题 JavaScript 测试最佳实践的指导 在这里尤其有用,因为它突出了人们经常忽视的过度 mock 和随后的脆弱测试问题 目标不是立即完成。它是团队在回归中花费最多的位置上稳步改进
在团队在回归中花费最多的位置上稳步改进
如果您的团队正在开发 Capacitor 或 Electron 应用程序,并需要对 JavaScript 变更进行更清洁的发布流程, Capgo 是一个值得考虑的选项。它为 CapacitorJS 和 Electron 应用程序提供实时更新,具有滚动控制和可观察性,使团队可以将坚实的单元测试与不必等待每个修复的商店审查就可以安全发布 Web 包变更的路径相结合。