你可能处于两种情况之一。要么你的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 - 有可读的匹配器 Appflow 或
- 钩子函数 用于设置和清理
- 异步支持 用于Promise和定时器
- 模拟工具 用于外部依赖
如果您的团队还需要了解测试自动化的更广泛视图,除了单元测试工作外,Capgo对应用交付工作流中的自动化测试提供了有用的概述 Jest vs Mocha快速对比.
Jest和Mocha代表了两种不同的哲学
Jest
是全能选项。它在第一天就带来了大多数团队需要的功能 Jest
Mocha 更具模块化。它为您提供了一个运行器,并希望您自己组装剩余的堆栈。
| 功能 | Jest | Mocha |
|---|---|---|
| 设置复杂度 | 对于大多数团队来说更低 | 因为您通常会添加断言和模拟库 |
| 断言 | 内置 | 通常与另一个库配对 |
| 模拟 | 内置 | 通常与另一个库一起使用 |
| 异步测试 | 内置且直接 | 支持,但依赖于周围的设置 |
| 覆盖工作流 | 通常集成到同一工具链中 | 经常需要拼凑 |
| 最佳匹配 | 新项目,希望保持一致性的团队 | 遗留堆栈,希望具有模块化控制的团队 |
实用规则: 如果您的团队需要询问哪个断言库和模拟库与运行器配对,很可能您想要 Jest。
我推荐的大多数团队
对于大多数现代项目,我会选择 Jest 除非代码库已经有强烈的理由继续使用 Mocha。对于大多数团队来说,我推荐的建议会随着应用程序包含 Capacitor 因为这些项目已经有足够的移动部分。减少测试工具的杂乱程度会带来快速的收益。 Mocha 在较旧的 Node.js 服务或长期存活的代码库中仍然有意义,因为其周围的生态系统已经稳定了。但是,对于一名中级工程师在从头开始设置一个强大的套件时,Jest 通常会比它创建的摩擦少。一个重要的范围说明。 Cypress 和 Playwright 是优秀的工具,但它们解决的是不同的问题。它们更适合于浏览器级别和端到端检查,而不是 JavaScript 单元测试应该居住的快速内部循环。
项目设置和您的第一个测试
Live Update Platform Capacitor
Live Update Platform Electron
A clean testing setup should be dull. If adding the first test feels complicated, the suite probably won’t stay healthy.

A simple Jest setup
Start with a JavaScript project that already has a package.jsonThen add Jest as a development dependency and wire up a test script.
{
"scripts": {
"test": "jest"
}
}
That’s enough for many projects. You can add more configuration later if your module system, transpilation, or monorepo structure requires it.
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 Write the test before the __CAPGO_KEEP_0__
Write the test before the code
writing the test first 在测试设置中,应该保持简单。如果添加第一个测试感觉复杂,测试套件可能不会保持健康。,组织测试 describe 和 it,并在其 expect(...) JavaScript单元测试指南 因为以测试为首的方法会改变您设计.
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);
});
});
每次都使用Arrange Act Assert
的 Arrange, Act, Assert 模式使测试保持可读,即使它们变得更加复杂。
- 准备 the input and any needed setup.
- Act 通过调用函数
- 断言 对结果
应用于验证辅助函数:
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);
});
});
小的测试会长久地保持。一个测试通常应该回答一个问题,而不是讲述整个工作流程。
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.
掌握模拟和异步Code
大多数应用code中的错误并不是来自于两个数字的加法。它们来自于code,它超出了自身的范围:网络请求、文件、插件API、定时器、IPC通道、存储层。
这就是模拟的作用。它让您控制边界,使测试可以专注于code的决策过程。

模拟边界,非所有内容
可维护性测试指南强调 单一行为覆盖 并且 一个强大的断言,它还警告说过度使用模拟会使测试变得脆弱并且紧密耦合到实现细节中,正如本文所总结的 TestRail关于可维护单元测试的文章.
这条警告在JavaScript中非常重要。团队经常从模拟所有导入模块开始,并最终测试函数是否正确调用其他函数,而不是测试真实行为。
模拟过多测试的坏目标:
- 助手A是否调用助手B
- 服务C是否调用序列化器D
- 内部私有函数是否运行两次
更好的目标:
- 函数返回的内容
- 它是否正确处理了依赖项失败
- 它是否将数据转换为期望的形状
更好的模式:Capacitor和Electroncode
在移动和桌面应用中,我更喜欢在原生或平台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集成等后台服务
For teams testing release logic and update paths in Capacitor apps, Capgo has a relevant guide on 对于测试Capacitor应用的发布逻辑和更新路径的团队,__CAPGO_KEEP_1__有一个相关的指南.
测试__CAPGO_KEEP_0__ OTA更新的模拟场景
异步流程测试
使用 async/await in tests when the code under test returns a promise. It’s clearer than callback-heavy patterns and easier to debug.
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');
});
当测试的__CAPGO_KEEP_0__返回一个Promise时。它比回调模式更清晰,更容易调试。
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');
});
也测试失败路径:
测试两条路径:成功路径和失败路径。在生产环境中,失败路径往往是用户记住的。
A test suite becomes useful when it stays useful after the code changes. That’s harder than writing a pile of passing tests.

测试套件的策略
使用测试分离的预算 70/20/10 一项实用的指南建议将测试分为 单元测试、集成测试和端到端测试与单元测试相比, 提供最快的反馈和最稳定的故障。同样的指导建议,一个完整的单元测试套件应该在秒内完成, 秒内完成,根据 OpenReplay测试指南.
我把它当作一个预算工具,而不是一种宗教。如果您的团队主要关注端到端测试,反馈会太慢。如果您只使用单元测试,会错过真正的系统边界。
For a Capacitor or Electron app, a healthy balance usually looks like this:
- 或Electron应用程序,一个健康的平衡通常如下: 单元测试
- 价格逻辑、权限规则、序列化、更新资格、功能标志和状态转换 集成测试
- 端到端测试 为关键旅程,如登录、购买流程、同步或更新提示,编写几条测试
覆盖率是一个手电筒,而不是目标
覆盖率报告在帮助您发现重要逻辑中未测试的 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插件交互
错误的方式来单元测试Capacitorcode是将本机插件直接拉入每个服务. 这会将测试套件与平台桥接起来.
更好的模式是一种薄的抽象:
// 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 单元测试 检查一个小逻辑片段的单元测试。 一个 集成测试 检查几个组件或服务是否正确工作。 一个 end-to-end测试 模拟用户在运行应用程序的完整用户体验。
使用单元测试快速确保业务规则。使用集成测试测试存储、插件包装器和IPC等接口。使用E2E测试仅用于可能严重影响应用程序工作流的场景。
我们应该追求全面覆盖
不。全面覆盖可能会导致团队优先考虑低价值测试。
覆盖率在揭示风险code时有用,但在工程师仅为了满足仪表板而添加浅表断言时则无用。如果测试套件脆弱,增加覆盖率也不会解决问题。
如何在现有代码库中添加测试
从变化发生的地方开始。不要冻结团队并宣布重写测试策略。
一个实际的顺序如下:
- 首先保护活跃code 通过在功能工作或bug修复期间添加测试来保护模块
- 提取纯粹的逻辑 从难以测试的文件中抽取业务规则,以便在不引入框架或运行时噪音的情况下测试业务规则
- 添加接口包装 在原生插件、网络客户端、文件系统调用和 Electron IPC 周围添加包装
- 拒绝脆弱的模式 当引入模拟时拒绝脆弱的模式。来自 JavaScript 测试最佳实践 的指导尤其有用,因为它突出了过度模拟和随后的脆弱测试的常见问题
目标不是立即完成。它是稳步改进在团队中最昂贵的回归测试的地方
如果您的团队发布 Capacitor 或 Capacitor JavaScript 应用需要更清晰的发布过程 Capgo CapacitorJS 和 Electron 应用提供实时更新,具有回滚控制和可观察性,团队可以将坚实的单元测试与更安全的发布 Web 包更改的路径配对起来,而无需等待每个修复的商店审核