跳过主要内容

JavaScript团队的实用Jest单元测试指南

从设置到CI,学习Jest单元测试。涵盖了模拟、TypeScript、覆盖率和最佳实践的实践教程,适用于JavaScript和Capacitor应用。

JavaScript团队的实用Jest单元测试指南

一个Capacitor版本可以通过其端到端检查而通过,但仍可能发布一个错误的发票计算、过时的特性标志或平台特定的分支。失败通常始于更早:一个单元测试仍然反映了旧的行为,一个模拟隐藏了更改的依赖项,或者CI在开发人员机器上运行的环境与CI运行的环境不同。等到bug到达手机或Electron桌面构建时,测试套件已经提供了信心但没有提供保护。

这就是为什么 Jest单元测试 应被视为一个活跃的工作流程决策,而不是仅仅是一个运行命令。有用的问题是实用的:开发者何时可以信任一个失败,哪些边界应该保持隔离,哪些覆盖应该在CI中,Jest是否仍然适合项目的模块系统和反馈预期?本指南将关注这些决策,涵盖Node服务、Web应用、Capacitor项目和Electron应用。 自动化测试在现代软件工作流程中的位置.

一个标题为Why Jest Still Matters的图表,概述了工作流程决策、陈旧的测试错误和现代测试策略。

目录

为什么 Jest 单元测试仍然在 2026 年有意义

Jest 仍然相关,因为它解决了更高级的断言语法之外的问题。它为团队提供了一个可重复的位置来验证商业逻辑、控制依赖边界、强制覆盖期望和在移动或桌面包中运行检查。这个工作流程在浏览器 shell、Capacitor WebView、Electron 渲染器和 Node 进程中都很重要,因为它们使用了不同的平台 API。

Jest 的采用也具有历史意义。Facebook 在 2011 为 JavaScript 聊天重写创建了它, 2014在公开了它的同时,OpenJS 基金会报告说它已经超过了 38,000 GitHub 个星和 2022 年每周 1700 万次下载, 到达超过 43,000 个星和 2024 年每周 2100 万次下载 (OpenJS 基金会的 Jest 项目历史). 这些数字并不能证明 Jest 对每个新仓库都是合适的,但它们解释了为什么团队经常继承成熟的生态系统、熟悉的惯例和大量现有的示例。

跳过单元测试以便于端到端覆盖看起来更便宜,直到每个小故障都需要进行全应用程序启动、设备设置、网络路径和平台特定诊断。E2E 测试对于发布关键旅程是有价值的,但它们并不是快速、专注的税务计算、更新清单、权限决策、存储适配器和错误映射的替代品。

实践规则: 保持 Jest 当其生态系统减少迁移风险并且您的套件为开发人员提供可信的反馈时。考虑另一个运行器当运行器本身已经成为每日瓶颈时。

本指南的其余部分遵循该工作流程。您将配置 Jest Across 环境、编写行为关注的测试、选择可维护的模拟、将检查连接到 CI 和覆盖、在大规模下减少不稳定性并做出明确的保留或切换决定。

在环境中安装和配置 Jest

从最小的配置开始,匹配运行时。一个 Node 服务通常需要 Jest 的默认环境和一个测试脚本。一个浏览器面向的 Capacitor 或 Electron 模块需要 DOM-like 全局变量,而 TypeScript 添加了一个转换决策,这可能会影响调试、模块兼容性和启动行为。

对于一个普通的 Node 项目,初始化包并安装 Jest 作为开发依赖:

npm init -y
npm install --save-dev jest
npx jest --init

生成的配置是一个起点,而不是设计的结论。审查测试环境、转换、模块别名和设置文件之前提交它。

选择 TypeScript 转换有意为之

ts-jest 在仓库已经依赖于 TypeScript 编译器行为并且开发者想要熟悉的诊断时是方便的。 @swc/jest 在转译速度很重要并且类型检查已经作为一个单独命令运行时是很有吸引力的。无论哪种方式都不会替代类型检查器,而且 ESM-heavy 包可能需要额外的配置,无论是使用哪种转换器。

选项 Node TypeScript Capacitor/Electron
环境 node node 或项目特定 jsdom 为 DOM 面向的 code
转换 通常没有 ts-jest@swc/jest TypeScript 转换加上 DOM 设置
ESM 处理 匹配包格式 验证转换器支持 检查插件依赖项和模块别名
典型设置 最小 jest.config.ts setupFilesAfterEnv, 模拟, 浏览器 API

A TypeScript 配置使用 ts-jest A TypeScript 配置如下

import type { Config } from 'jest'

const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
  clearMocks: true,
  collectCoverageFrom: ['src/**/*.{ts,tsx}'],
}

export default config

对于基于 Babel 的 JavaScript 或混合仓库,保持 Babel 文件明确:

module.exports = {
  presets: [
    ['@babel/preset-env', { targets: { node: 'current' } }],
    '@babel/preset-typescript',
  ],
}

仅添加浏览器 shim,code 需要

Capacitor 和 Electron 测试频繁导入 code,它期望 window.matchMediaIntersectionObserver. A setup file can provide controlled shims without pretending that Jest is a real device or desktop shell:

Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: (query: string) => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: () => {},
    removeListener: () => {},
    addEventListener: () => {},
    removeEventListener: () => {},
    dispatchEvent: () => false,
  }),
})

class MockIntersectionObserver {
  observe() {}
  unobserve() {}
  disconnect() {}
}

Object.defineProperty(window, 'IntersectionObserver', {
  writable: true,
  value: MockIntersectionObserver,
})

context transformIgnorePatterns and the package’s published format. Capacitor plugins can expose this problem when Jest ignores a dependency that still needs transformation. Jest’s newer releases have improved startup and memory behavior, but watch feedback can still trail ESM-focused alternatives in large projects (2026 Jest 和 Vitest 比较). Treat experimentalVMModules context

对于一个专注于JavaScript的设置教程,请使用 Capgo的JavaScript单元测试指南。完成安装后使用一个真实的验证命令:一个通过的烟雾测试确认Jest可以加载项目。它并不能确认你的生产和测试模块图形行为相同,所以在那些路径重要的地方保留一个ESM、DOM和插件导入测试。

npx jest --runInBand

编写第一个可靠的单元测试

一个有用的单元测试描述了一个可观察的行为在一个受控的上下文中。

Arrange, Act, Assert 模式保持了这个意图可见:准备输入和依赖项,调用公共函数,然后验证结果或外部可见的效果。 假设一个发票模块导出这个函数:

测试应该关注财务行为,而不是名为

export function calculateInvoiceTotal(
  subtotal: number,
  taxRate: number,
  discountRate: number,
): number {
  const discounted = subtotal * (1 - discountRate)
  return Math.round(discounted * (1 + taxRate) * 100) / 100
}

Each discounted:

import { calculateInvoiceTotal } from './calculateInvoiceTotal'

describe('calculateInvoiceTotal', () => {
  it('applies percentage discount before tax', () => {
    const subtotal = 100
    const taxRate = 0.2
    const discountRate = 0.1

    const total = calculateInvoiceTotal(subtotal, taxRate, discountRate)

    expect(total).toBe(108)
  })

  it('rounds the final amount to currency precision', () => {
    const total = calculateInvoiceTotal(19.99, 0.2, 0)

    expect(total).toBe(23.99)
  })
})

的局部变量。如果后续的重构改变内部计算但保留了契约,这些测试应该仍然有用。 it __CAPGO_KEEP_0__的JavaScript单元测试指南

一个图表,概述了软件开发中编写可靠单元测试的四个基本步骤。

异步测试需要同样的纪律。 Jest 的 resolvesrejects 使期望的承诺结果显式:

it('returns an invoice from the API', async () => {
  await expect(fetchInvoice('invoice-123')).resolves.toMatchObject({
    id: 'invoice-123',
  })
})

it('rejects when the invoice is missing', async () => {
  await expect(fetchInvoice('missing')).rejects.toThrow('Invoice not found')
})

创建一个未等待或返回的拒绝期望是危险的错误。 Jest 专注的指导会忘记 awaitreturn 避免三种习惯,它们会产生脆弱的自信心:测试私有助手:除非助手代表一个有意义的公共边界,否则测试导出的行为

测试私有助手:

  • 测试私有助手: 测试私有助手:
  • 断言内部状态: 优先使用返回值、发出的事件、持久化记录或可见输出。
  • 过度使用调用断言: toHaveBeenCalled() 单独使用断言并不能说明太多。检查相关参数和结果行为。

PR 检查清单可以保持简短:

  • 每个测试是否覆盖一个行为?
  • 测试是否遵循 Arrange、Act、Assert?
  • 是否已等待异步期望?
  • 是否仅在明确的边界处模拟依赖项?
  • 测试是否能在内部重构后继续运行?

对于组件特定示例,请参阅 Capgo的React 单元测试指南 遵循相同的行为原则对渲染输出和用户交互进行应用。

有效的模拟策略

模拟变得困难,当套件增长时,因为每个捷径都会创建一个维护义务。手写的 jest.fn() can be exactly right for a callback. A module replacement can isolate an SDK. A network interceptor can preserve more of the application’s real request behavior. The choice should follow the seam you’re testing.

Consider a payment validator that calls a Stripe SDK, records an audit event, and reaches an HTTP risk service. A focused unit test might spy on the logger, replace the payment SDK, and intercept the risk request. Each technique controls a different boundary.

策略 设置成本 准确度 维护负担 最佳匹配
jest.fn()jest.spyOn() 专注 当在本地时较低 回调函数、日志记录器、注入的服务
jest.mock() 中等 较低至中等 可以快速增长 具有昂贵副作用的 SDK、模块
MSW 中等 HTTP 边界处更高 集中化 请求行为、错误、响应契约

手动间谍用于本地决策

在依赖项已经注入的情况下,使用间谍,当测试需要观察或控制一个交互时:

const audit = {
  record: jest.fn(),
}

const result = await validatePayment(input, {
  paymentClient,
  audit,
})

expect(audit.record).toHaveBeenCalledWith(
  expect.objectContaining({ event: 'payment.validated' }),
)
expect(result.status).toBe('approved')

重置状态之间的案例。共享状态是Jest故障的常见来源,指导建议结合清除模拟以防止调用计数和状态泄露( beforeEach jest单元测试精通模块模拟用于重量级SDK).

一个Stripe或原生插件模块通常在加载时立即执行设置。替换该模块时,加载生产实现将引入凭证、原生绑定或无关行为:

测试仍应断言应用程序返回的值。一个通过的__CAPGO_KEEP_0__调用断言没有意义的结果可以证明仅仅是模拟配置。

jest.mock('stripe', () => ({
  payments: {
    authorize: jest.fn(),
  },
}))

The test should still assert the value returned by the application. A passing SDK call assertion without a meaningful result can prove only that the mock was configured.

对于__CAPGO_KEEP_0__拥有请求构造、响应解析、重试或错误转换的MSW可以拦截HTTP层并保留请求路径:

For code that owns request construction, response parsing, retries, or error translation, MSW can intercept the HTTP layer while preserving the request path:

server.use(
  http.post('/risk/check', async () => {
    return HttpResponse.json({ decision: 'review' })
  }),
)

本文将介绍jest单元测试的精通 fetch 模块模拟用于重量级SDKs

尽在边界,不要侵入核心

过度模拟会导致测试通过,但集成时的连接问题仍然存在。另一方面,模拟不足会将实际的网络、文件系统、时钟或数据库引入单元测试中,导致测试速度慢且不确定。将纯粹的逻辑与所有I/O隔离开来,然后将边界验证移动到契约或集成测试中,这里界面才真正重要

集成Jest与CI和覆盖率门槛

CI应该回答两个不同的问题。首先,快速的单元测试是否拒绝了不安全的更改?其次,较慢的集成检查是否确认了重要的边界仍然有效?将所有测试合并到一个命令中会使反馈更难解读,并鼓励开发人员绕过测试

一个GitHub Actions工作流程可以锁定Node运行时,使用锁文件进行依赖项缓存,并使用Jest的CI模式运行单元测试

name: test

on:
  pull_request:
  push:

jobs:
  unit:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node: [20, 22]
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
          cache: npm
          cache-dependency-path: package-lock.json

      - run: npm ci
      - run: npm run test:unit, --ci --coverage

      - uses: actions/upload-artifact@v4
        with:
          name: coverage-${{ matrix.node }}
          path: coverage/

包脚本可以将快速测试与集成工作分开

{
  "scripts": {
    "test:unit": "jest --runInBand tests/unit",
    "test:integration": "jest --runInBand tests/integration"
  }
}

使用覆盖率门槛来反映风险,而不是习惯性地选择一个普遍的数字

module.exports = {
  collectCoverageFrom: ['src/**/*.{js,ts,tsx}'],
  coverageThreshold: {
    global: {
      branches: 70,
      functions: 80,
      lines: 80,
      statements: 80
    }
  }
}

这些值是配置的例子,而不是经过验证的行业标准。从当前基线中设置实际的门槛,然后在团队添加有意义的覆盖率时提高它。过于严格的门槛可能会阻止紧急修复,如果它测量的是生成的或低风险的code。过于宽松的门槛可能会让关键路径无视

来自https://docs.github.com/assets/images/help/repository/actions-illustration.png的截图

对于大型仓库,应在测试隔离得到保证后才使用矩阵分片。每个分片需要明确对其报告的拥有权,最后的状态应使失败在 pull 请求中可见,而不是将其埋在日志中。团队还可以将 HTML 覆盖率作为 artifact 上传并发布数据到 Codecov 或 Coveralls,假如服务已正确配置以合并报告。 lcov 数据可以上传到 Codecov 或 Coveralls,假如服务已正确配置以合并报告。

阅读 CI 运维纪律 应与工作流设计并行,尤其是当多个应用共享一个仓库时。Capgo’s CI/CD 集成测试指南在测试状态必须连接到移动构建和发布自动化时很有用。 大规模 Jest 单元测试的可信度 一个大型 Jest 单元测试套件可以持续通过测试,但测试的内容可能是错误的。测试的可信度会下降,当测试依赖于实现细节、快照超出了有价值的审查范围或共享 mock 改变了行为远离配置它们的测试时。

快照需要明确的拥有权。它们在结构呈现为真实契约时(例如稳定的组件或序列化消息)很有效。它们会产生噪音,当开发人员批准广泛更新而没有检查输出时。请有意地重新生成它们,审查 diff,并删除不再保护有意义行为的文件。

使用层次结构而不是强制 Jest 拥有所有内容

为每个测试层分配一个狭窄的任务:

CI/CD 集成测试指南

大规模 Jest 单元测试的可信度

  • 纯单元测试: 验证确定性的计算、解析器、减少器、策略决策和错误映射,
  • 契约测试: 检查模块边界、适配器形状、请求负载和面向插件的行为。
  • 薄的端到端覆盖: 在Web、Capacitor或Electron shell中,

通过这种安排,单元测试保持快速,同时检查mocks不能代表生产的边界。它还避免了常见的移动失败模式:

测试整个本机更新或权限流程, it() 而不是隔离JavaScript决策逻辑。

A passing test should explain what users or neighboring modules can rely on, not how today’s code happens to be arranged.

这样失败就可以诊断。只有当顺序属于契约时,才断言调用顺序。一个可查询的假设,例如一个内存仓库,通常比硬编码的返回值提供更好的反馈。硬编码的返回值仅复制当前实现。

CI 监控面板中跟踪不稳定性。如果测试失败而没有任何code的更改,保留失败的证据,隔离共享状态,控制时间和随机性,并在运行者过载时减少工作者压力。从CI机器上获取的测量值来设置工作者限制,因为额外的并行性会增加争用并使套件变慢。将工作者设置与运行者配置文档化,以便未来更改保持有意图。

决策框架和团队的下一步行动

当一个仓库已经有稳定的套件、建立的转换和模拟,以及一个重视迁移安全性而不是运行者实验的团队时,Jest仍然是一个合理的默认值。比较替代方案时,新项目是ESM-first、native反馈是优先级、或watch-mode重复运行经常中断开发。发布的比较结果可能会根据仓库架构和转换工作而有很大差异,所以将其视为方向性而不是承诺。

使用四个标准:

  1. 现有工具: 保持 Jest 当配置、测试工具和 CI 规范已经工作时。
  2. 模块格式: 当 ESM-only 依赖反复要求异常或自定义工作-around 时,重新考虑运行者。
  3. 反馈期望: 在实际仓库中测量代表性的 watch 变化,而不是一个空的 demo 项目。
  4. 团队能力: 团队可以配置和调试的熟悉的运行者可能比一个速度更快但被使用得不当的工具产生更好的结果。

Vitest、Node内置的测试运行器,以及Playwright分别满足不同的需求。Playwright主要适用于浏览器端的E2E测试,Node的运行器则适用于专注于Node服务的测试。Vitest通常适用于绿色场景下的ESM和Vite项目。Jest仍然适用于依赖成熟的模拟、转换和现有CI约定的团队。选择一个能保持可信界限,同时保持反馈可用的测试运行器。

实用90天重置计划

第一月,审计测试套件。 将负责人分配给清理掉易碎测试、死的快照、实现耦合的断言和执行真实I/O的测试。输出结果应该是一个清单,列出需要移除、修复和需要集成覆盖的界限。

第二月,标准化设计。 添加共享的测试工具、文档模拟约定以及对重要包的覆盖率报告。记录哪些包有门槛以及哪些行为仍然缺乏专注的测试,以便基线保持可审查。

第三月,稳定交付。 调节CI工人、分离单元和集成任务、设置基于风险的覆盖率底线以及文档约定在一个活跃的 TESTING.md.管道应该报告可执行的失败,而新测试应该遵循相同的界限和模拟规则。

定期检查测试套件。移除过时的快照、冗余的设置和不再符合生产行为的模拟。一个低价值的测试可能比再次使用另一个断言更安全地删除。跟踪CI中的易碎故障,保存证据,隔离共享状态,控制时间和随机性,并在出现争议时减少工作者的压力。从CI机器上获取测量值并将它们与运行器配置一起保存。

这些实践属于更广泛的 软件开发最佳实践。对于目标Capacitor或Electron用户的测试JavaScript修复,Capgo可以通过目标通道传递签名的JavaScript、CSS、配置和资产包,具有回滚保护和发布可观察性,且不需要为每个Web层修复申请商店审查。

如果您的团队部署Capacitor或Electron应用,请访问 Capgo to assess how live JavaScript updates fit controlled channels, staged rollouts, and rollback protection. Start by documenting Jest boundaries and CI gates, then define where Capgo belongs in the recovery path for fixes that need prompt delivery.

实时更新 Capacitor 应用

当 web 层 bug 活跃时,通过 Capgo 将修复推送给用户,而不是等待几天的 app store 审批。用户在后台接收更新,而原生变化仍然在正常的审批路径中。

人性化支持从 Martin

立即开始

最新博客文章

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