跳过主要内容

JavaScript单元测试:2026年全面指南

掌握2026年JavaScript单元测试指南。涵盖Jest、Mocha、设置、模拟、CI和针对Capacitor & Electron应用的技巧。

JavaScript单元测试:2026年全面指南

你可能处于两种情况之一。要么你的JavaScript项目几乎没有测试,每次重构都感到风险很大,要么你已经有测试,但其中一半测试很慢、脆弱且难以信任。

情况会在 CapacitorElectron 应用程序。一个简单的功能可以触及共享的业务逻辑、浏览器API、原生插件、本地文件、IPC和远程服务在同一流程中。如果您以错误的方式测试这些部分,测试套件就会变成一个假依赖项的迷宫。如果您以正确的方式测试它们,您会获得对破坏逻辑的快速反馈。

好的单元测试JavaScript工作并不以聪明的匹配语法开始。它从一个严格的界限开始:测试纯粹的逻辑直接,隔离副作用,并避免编写测试以便在重命名内部函数时立即崩溃。

目录

选择 JavaScript 测试框架

专业的 JavaScript 项目需要一个真正的测试运行器。临时脚本和手动控制台检查在多个工程师处理同一代码库时无法扩展。您需要测试发现、断言、异步处理、模拟和在本地开发和 CI 中一致地运行所有内容的方式。

当前指导方针正在趋向于小组的主流选项。 Jest、Mocha 和 Jasmine 经常被突出为主要框架, Jest 经常被单独提及的内置测试结构、断言、模拟和异步支持的包装器,正如本文所示 Pluralsight JavaScript 测试实验室.

JavaScript 测试框架的比较图表,包括 Jest、Mocha、Cypress 和 Playwright

为什么框架不是可选项

团队的第一个错误是将单元测试视为一项次要活动。这通常导致文件命名不一致、 nobody 记得的自定义断言和只有一个人理解的助手

框架给你一个共享的语言:

  • 测试结构describetest 断言 it
  • 有可读的匹配器 with
  • 函数钩子 用于设置和清除
  • 支持异步 用于Promise和定时器
  • 模拟工具 用于外部依赖

如果您的团队还需要了解测试自动化的更广泛视图,除了单元测试工作之外,Capgo 提供了一个有用的概述 应用交付工作流程中的自动化测试.

Jest vs Mocha快速对比

Jest和Mocha代表了两种不同的哲学

Jest 是全能的选择。它在第一天就带来了大多数团队需要的内容
Mocha Mocha

功能 Jest Mocha
设置复杂度 大多数团队 因为你通常需要添加断言和模拟库
断言 通常与另一个库一起使用 模拟
mocking 内置的 通常与另一个库一起使用
异步测试 内置且直接 支持,但依赖于周围的设置
覆盖工作流 通常集成到同一个工具链中 通常需要更多的组装
最佳匹配 context:Capgo Builder / native cloud build product page. Role: Short UI label or navigation item. Message key `native_build_builder_compare_fit_feature` (Native Build Builder Compare Fit Feature). 新项目,希望保持一致性的团队

遗留堆栈,希望具有模块化控制的团队 如果您的团队需要询问哪个断言库和模拟库与运行器配对,您可能希望使用 Jest。

我推荐的大多数团队

对于大多数现代项目,我会选择 Jest 除非代码库已经有强烈的理由保持在 Mocha 上。这种推荐在应用程序中包含 Capacitor Capacitor 因为这些项目已经有足够的移动部分。减少测试工具的杂乱程度会带来快速的收益。Mocha 在较旧的 Node.js 服务或长期存活的代码库中仍然有意义。然而,对于中级工程师设置一个强大的套件从头开始,Jest 通常比它创建的摩擦要少。

一个重要的范围说明。 Cypress 和 Playwright 是优秀的工具,但它们解决的是不同的问题。它们更适合于浏览器级别和端到端检查,而不是 JavaScript 单元测试应该居住的快速内部循环。

项目设置和您的第一个测试

Electron

A clean testing setup should be dull. If adding the first test feels complicated, the suite probably won’t stay healthy.

A man wearing glasses working on a programming project on a laptop at a wooden desk.

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

Write the test before the code

writing the test first writing the test first,组织测试 describeit,并在其 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 Arrange, Act, Assert

  1. 模式使测试保持可读,即使它们变得更加复杂。 输入和所需的设置。
  2. 动作 通过调用函数
  3. 断言 在验证辅助函数上应用

小的测试会随着时间而保持新鲜。一个测试通常应该回答一个问题,而不是讲述整个工作流程。

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和Electron项目来说,这种纪律更为重要,因为您的纯粹逻辑通常与native或桌面集成放在一起__CAPGO_KEEP_0__. 保持业务规则可测试而不依赖于平台运行时,第一个测试不会是最后一个有用的测试。

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中的错误并不是来自于添加两个数字。它们来自于__CAPGO_KEEP_1__,它超出了自身的界限:网络请求、文件、插件API、定时器、IPC通道、存储层。

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.

A whiteboard diagram illustrating a microservices architecture with APIs, data stores, external services, and event-driven data flow.

模拟边界,不是所有的

可维护的测试指导强调 单一行为覆盖一个强大的断言,它还警告说过度使用模拟会使测试变得脆弱并紧密耦合到实现细节中,正如本文所总结的 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集成或其他后端服务后面都有一个薄的适配器。单元测试击中服务层,而不是直接击中运行时。

对于测试Capacitor应用的发布逻辑和更新路径的团队,Capgo有一个相关的指南关于 使用模拟场景测试CapacitorOTA更新.

如果您的团队仍在规范异步测试风格,快速的指南会有所帮助:

测试异步流程而不出现不稳定性

使用 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.

一个测试套件只有在它保持有用性,即使__CAPGO_KEEP_0__发生变化时才是有用的。写一堆通过的测试比这更容易。

一个图表,展示了通过全面测试覆盖和可维护的测试套件来构建健壮的软件的策略。

使用测试分离的预算 70/20/10 一项实用的指南建议将测试分成 单元测试、集成测试和端到端测试,通过单元测试获得最快的反馈和最稳定的故障。同样的指导建议,一个完整的单元测试套件应该在 秒内完成,秒内完成, ,根据OpenReplay测试指南 我把它当作一个预算工具,而不是一种宗教。如果大部分的努力都花在了端到端测试上,团队会等待太长时间才能获得反馈。如果一切都是单元测试,你会忽略真实的系统边界。.

对于一个

Capacitor

  • 或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提供了一个实用的指南,用于设置Capgo应用的CI/CD Capacitor插件交互的测试.

错误的方式是通过将本机插件直接拉入每个服务来测试Capacitor __CAPGO_KEEP_1__。这会将测试套件与平台桥接耦合。

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。不要在测试中模糊它们。

可靠的设置通常分离:

  • 渲染器单元测试 用于视图模型、状态、格式化和 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 测试最佳实践 的指导是特别有用的,因为它突出了过度模拟和随后的脆弱测试的常常被忽略的问题

目标不是立即完成。它是稳步改进在团队中最昂贵的回归中


如果您的团队发布 CapacitorCapacitor 应用和需要一个更干净的JavaScript变化的发布过程 Capgo CapacitorJS和Electron应用提供实时更新、滚动控制和可观察性,团队可以将坚实的单元测试与不必等待每个修复的商店审核的更安全的路径结合起来,来发布Web包更新

Capacitor应用的实时更新

当web层bug出现时,通过Capgo立即发布修复,而不是等待几天的app store审批。用户在后台接收更新,而native层的更改仍然在正常的审批路径中。

来自Martin的人性化支持

立即开始

最新博客文章

Capgo为您提供创建真正专业的移动应用所需的最佳见解。