你可能处于两种情况之一。要么你的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_KEEP_0__ 对于外部依赖项
如果您的团队还需要测试自动化的更广泛的视图,除了单元测试之外,Capgo 有一个有用的概述 在应用交付工作流程中的自动化测试.
Jest 与 Mocha 的快速对比
Jest 和 Mocha 代表了两种不同的哲学
Jest 是全能的选择。它带来了大多数团队在第一天需要的内容
Mocha 是更模块化的。它给了你一个运行器,并期望你自己组装剩余的堆栈
| 功能 | Jest | Mocha |
|---|---|---|
| 测试复杂度 | 对于大多数团队来说 | 因为你通常会添加断言和模拟库 |
| 断言 | 内置 | 通常与另一个库一起使用 |
| 模拟 | 内置 | 通常与另一个库一起使用 |
| 异步测试 | 内置且直接 | 支持,但取决于周围的设置 |
| 覆盖工作流程 | 常常与同一工具链集成 | 通常是拼凑起来的 |
| 最佳匹配 | 适合新项目、追求一致性的团队 | 适合遗留堆栈、追求模块化控制的团队 |
实践规则: 如果您的团队需要询问哪个断言库和模拟库与运行器配对时,您可能希望使用 Jest。
我推荐的大多数团队
对于大多数现代项目,我会选择 Jest 除非代码库已经有强烈的理由继续使用 Mocha。这种推荐在应用程序中包含时会变得更加强大 Capacitor 或 Electron因为那些项目已经有足够的移动部分。减少测试工具的杂乱程度会带来快速的收益。
摩卡仍然在旧的 Node.js 服务或长期存活的代码库中有意义,因为周围的生态系统已经稳定。但是,对于一名中级工程师在从头开始设置一个健全的套件时,Jest通常比它造成的摩擦少。
一个重要的范围说明。Cypress 和 Playwright 是优秀的工具,但它们解决的是不同的问题。它们更适合于浏览器级别和端到端检查,而不是快速的内部循环,JavaScript 单元测试应该在那里工作。
项目设置和您的第一个测试
一个健康的测试套件应该是无聊的。如果添加第一个测试感觉复杂,套件可能不会保持健康。

一个简单的 Jest 设置
从一个已经有一个 package.json然后将 Jest 添加为开发依赖项并连接一个测试脚本。
{
"scripts": {
"test": "jest"
}
}
对于许多项目来说,这已经足够了。您可以在需要时添加更多的配置项,例如您的模块系统、转译或多包结构。
如果您正在本地构建一个Capacitor应用,并希望在添加测试之前将开发环境保持有序,Capgo关于 设置Capacitor本地环境的指南 在__CAPGO_KEEP_0__之前编写测试
在写测试之前编写code
编写测试 ,并 describe , it并将检查项框架在 expect(...) 断言中 在其JavaScript单元测试指南中.
因为这很重要,测试驱动的变化会影响你设计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模式 模式使测试保持可读,即使它们变得更加复杂。
- 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 位于同一个位置。保持业务规则可测试,而不依赖于平台运行时,第一条测试不会是最后有用的测试。
掌握 Mocks 和异步 Code
大多数应用 code 中的错误并不是来自于添加两个数字。它们来自于 code,它超出了自身的界限:网络请求、文件、插件 API、定时器、IPC 通道、存储层。
这就是 Mocks 的作用。它让您控制界限,使测试可以专注于 code 的决策过程。

模拟界限,而不是模拟所有内容
可维护的测试指导强调 单一行为覆盖 和 一个强大的断言每个测试,它还警告说过度使用模拟会使测试变得脆弱并紧密耦合到实现细节中,如本文所述 TestRail 文章关于可维护的单元测试.
在 JavaScript 中,这个警告非常重要。团队经常从模拟每个导入的模块开始,最后会测试函数是否以正确的顺序调用其他函数,而不是测试真实行为。
模拟过多测试的坏目标:
- 助手 A 是否调用助手 B
- 服务 C 是否调用序列化器 D
- 内部私有函数是否运行两次
更好的目标:
- 函数返回的值是什么
- 它是否正确处理依赖项失败
- 它是否将数据转换为预期的形状
一个更好的模式:Capacitor 和 Electron code
In mobile and desktop apps, I prefer a wrapper layer around native or platform APIs. Then unit tests mock the wrapper, not the platform itself.
示例结构:
// 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 更新的 mock 场景.
快速的教程可以帮助您的团队仍然正常化异步测试风格:
测试异步流程而不引起不稳定性
使用 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');
});
测试两条路径:happy path 和 ugly path。 在生产环境中,ugly path 是用户通常记住的那一条。
高级测试策略
一个测试套件只有在它在code变化后仍然有用时才会有用。写一堆通过的测试比这更容易。

使用测试分离作为预算
一项实用的指南建议将测试分成 70/20/10 单元测试、集成测试和端到端测试 ,其中单元测试提供最快的反馈和最稳定的故障。同样的指导建议说,一个完整的单元测试套件应该在10秒钟内 完成,并且预提交检查应该在5秒钟内 ,根据这个5秒钟内 开始使用 OpenReplay 的测试指南.
我把它当作一个预算工具,而不是一种宗教。如果大部分的努力都花在了端到端测试上,团队会等待太久才能获得反馈。如果所有的测试都是单元测试,你会错过真正的系统边界。
对于一个 Capacitor 或 Electron 应用程序,一种健康的平衡通常如下:
- 单元测试 用于价格逻辑、权限规则、序列化、更新资格、特性标志和状态转换
- 集成测试 用于存储适配器、插件包装器和 IPC 协议
- 端到端测试 用于几个关键的旅程,如登录、购买流程、同步或更新提示
覆盖率是一个手电筒,而不是目标
覆盖率报告在帮助您-spot 未测试的 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的方式是将原生插件直接拉入每个服务。这样会将测试套件耦合到平台桥接上。
更好的模式是使用薄的抽象:
// 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 单元测试常见问题
单元测试、集成测试和端到端测试之间的区别是什么
A 单元测试 单元测试检查一个小的逻辑单元的隔离情况。一个 集成测试 检查几个组件或服务是否正确地协同工作。一个 端到端测试 检查通过运行应用程序的用户旅程。
使用单元测试快速获得商业规则的信心。使用集成测试检查存储、插件包装器和IPC等缝隙。使用E2E测试谨慎地检查那些如果它们破坏会严重伤害的工作流。
我们应该追求全面覆盖
不。全面覆盖可能会推动团队朝着低价值测试的方向发展。
覆盖率在揭示风险的code时是有用的,但当工程师仅仅为了满足仪表板而添加浅表的断言时,它就不再有用。如果您的测试套件脆弱,更多的覆盖率也无法拯救它。
如何在现有代码库中添加测试
开始在变化的地方。不要冻结团队并宣布巨大的测试策略重写。
实践的顺序如下:
- 首先保护活跃的code 通过在功能工作或bug修复中添加测试来保护模块
- 提取纯粹的逻辑 从难以测试的文件中提取业务规则
- 添加接口包装 在原生插件、网络客户端、文件系统调用和Electron IPC周围
- 拒绝脆弱的模式 当引入mock时,拒绝脆弱的模式。指导来自 JavaScript 测试最佳实践 特别适用于此场景,因为它突出了人们经常忽视的过度模拟问题以及随后的脆弱测试
目标不是立即完成。它是稳步改进在哪里回归成本团队最多的地方
如果您的团队发布 Capacitor 或 Electron 应用和需要更干净的 JavaScript 变更的发布过程 Capgo 是看看的一种选择。它为 CapacitorJS 和 Electron 应用提供实时更新,具有滚动控制和可观察性,使团队可以将坚实的单元测试与不必等待每个修复在商店审查的更安全的路径配对以发布 web 包更改