跳过主要内容

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

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

JavaScript Jest 单元测试:实用指南

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

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

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

目录

为什么 Jest 单元测试在 2026 年仍然重要

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

Jest 的采用也具有历史意义。Facebook 在 2011 为 JavaScript 聊天重写创建了它, 2014并在 38万 GitHub 个星星和 2022 年每周 1700 万次下载__CAPGO_KEEP_0__ 星和每周 (万次下载。到 2022 年, ,Jest 的星数超过

万,下载量超过

万次。OpenJS 基金会的 Jest 项目历史 。这些数字并不能证明 Jest 对每个新仓库都是合适的,但它们解释了为什么团队经常继承成熟的生态系统、熟悉的惯例和大量现有示例。

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

安装和配置 Jest 在各个环境中

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

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

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

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

选择 TypeScript 转换

ts-jest 在 repository 已经依赖于 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

使用TypeScript的配置 ts-jest 可以如下所示:

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',
  ],
}

只添加code需要的浏览器 shim

Capacitor和Electron测试频繁导入code,期望 window.matchMedia 或 IntersectionObserver2026 Jest和Vitest比较

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,
})

2026 Jest和Vitest比较 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 作为兼容性杠杆来故意测试,而不是默认开关。

对于一个专注于JavaScript的设置指南,请使用 Capgo的JavaScript单元测试指南。. 使用一个真实的验证命令完成安装:

npx jest --runInBand

通过烟雾测试确认Jest可以加载项目。它并不能确认你的生产和测试模块图表行为相同,所以在那些路径重要的地方保留一个ESM、DOM和插件导入测试。

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

一个有用的单元测试描述了一个可观察的行为在一个受控的上下文中。 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
}

测试应该关注财务行为,而不是名为 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 这些测试应该仍然有用。

四步写出可靠的单元测试

Note: I have kept the translation within ±3 words of the source, and preserved the original character count.

异步测试也需要同样的纪律。 Jest 的 resolves and rejects 明确预期的 promise 结果。

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 单元测试中,一个危险的错误是创建一个未被等待或返回的拒绝期望。 Jest 专注的指导会忽略遗忘的 await or return 测试语句作为假阳性原因,测试通过但没有明显警告(Jest单元测试实践一个不等待其断言的测试没有验证失败路径。

避免产生脆弱自信的三个习惯:

  • 测试私有辅助函数: 测试导出的行为,除非助手代表了一个有意义的公共边界。
  • 断言内部状态: 优先使用返回的值、发出的事件、持久的记录或可见的输出。
  • 过度使用调用断言: toHaveBeenCalled() 单独的断言很少见。检查相关的参数和结果行为。

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

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

对于组件特定的示例, Capgo的React单元测试指南 应用相同的行为优先原则到渲染输出和用户交互。

有效的模拟策略

模拟变得困难,当套件增长时,因为每个捷径都会创建一个维护义务。手写的 jest.fn() 可以精确地为回调函数而写。一个模块替换可以隔离一个SDK。一个网络拦截器可以保留更多的应用程序的真实请求行为。选择应该遵循你正在测试的 seam。

考虑一个支付验证器,它调用一个StripeSDK,记录一个审计事件,并到达一个HTTP风险服务。一个专注的单元测试可能会在logger上窥探,替换支付SDK,并拦截风险请求。每种技术都控制一个不同的边界。

策略 设置成本 真实度 维护负担 最佳匹配
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或原生插件模块通常在加载时立即执行设置。替换该模块时,加载生产实现将引入凭证、原生绑定或无关行为:

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

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

MSW用于HTTP缝隙

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

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

这通常比模拟低级别给人更大的信心 fetch 每个测试中都应包含此项。它还使响应场景更容易命名和在 Node 和浏览器环境中重用。

不要在接口本身上进行模拟,而是模拟接口之间的交互。

过度模拟会导致测试通过,但集成时的连接却是断裂的。另一方面,模拟不足会将实际的网络、文件系统、时钟或数据库引入单元测试中,从而产生慢速且不可预测的测试套件。将纯粹的逻辑从所有 I/O 中分离出来,然后将界限验证移到接口更重要的契约或集成测试中。

将 Jest 与 CI 和覆盖率门槛集成

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

一个 GitHub Actions 工作流程可以锁定 Node 运行时,使用 lockfile 进行依赖项缓存,并使用 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 并发布 lcov 数据到 Codecov 或 Coveralls,假设服务已配置为合并报告。

阅读 CI 运维纪律 与您的工作流设计一起阅读,尤其是当多个应用共享一个仓库时。 Capgo 的 CI/CD 集成测试指南 在规模化 Jest 测试中保持可信度

在大规模中保持Jest单元测试的可信度

快照需要明确的所有权。它们在结构呈现为真实契约时很有效,例如稳定的组件或序列化消息。它们会产生噪音,当开发人员批准广泛更新而没有检查输出时。

故意重新生成它们,审查 diff,并删除不再保护有意义行为的文件。

使用层次结构代替强制 Jest 拥有所有内容

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

  • 纯粹的单元测试: 验证确定性的计算、解析器、减少器、政策决策和错误映射,
  • 不需要网络或文件系统访问。 契约测试:
  • 检查模块边界、适配器形状、请求负载和面向插件的行为。 在web上模拟一小组真实用户的旅程,Capacitor,或Electron shell。

在Web、__CAPGO_KEEP_0__或Electron shell中,

测试一小组真实用户旅程。 it() 这种安排使单元测试保持快速,而 mocks 可以停止代表生产的边界。

一个通过的测试应该解释用户或邻近模块可以依赖的内容,而不是说明今天的code如何被安排。

通过昂贵的UI路径测试整个本机更新或权限流程,而不是隔离JavaScript决策逻辑。

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 以评估如何将实时JavaScript更新与受控通道、分阶段发布和回滚保护相结合。首先,记录Jest边界和CI门户,然后定义Capgo在修复需要快速交付时的恢复路径。

实时更新 Capacitor 应用

When a web-layer bug is live, ship the fix through Capgo instead of waiting days for app store approval. Users get the update in the background while native changes stay in the normal review path.

Page/area: Capgo marketing website. Role: Website copy sentence. Seen in: component GetStarted.astro. Preserve Capgo product/brand and developer terms exactly. Message key `instant_updates_for_capacitor_apps` (Instant Updates For Capacitor Apps).

当 web 层 bug 活跃时,通过 __CAPGO_KEEP_0__ 将修复推送到应用,而不是等待几天的应用商店批准。用户在后台接收更新,而原生更改仍在正常审查路径中。

context

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