跳转至正文

React 单元测试:实战全流程指南

从环境搭建到 CI/CD,全面掌握 React 单元测试。本指南涵盖 Jest、RTL、hooks、异步 code、mock,以及构建健壮跨平台应用的最佳实践。

“React 单元测试:实用端到端指南” Capgo 博客插图

午饭前,你推了一个小 UI 改动。看起来毫无风险:改了个按钮文案,简化了一处条件渲染,helper hook 还多了一个分支。PR 很干净,review 很快,部署就发出去了。

一小时后,客服反馈某个平台的登录挂了。Web 端看着正常,桌面端 shell 里有一条过时的渲染路径。移动端 build 在异步状态变更后表现不一样。没人发现,是因为 code 有测试,但没测对,更别说围绕这些测试的可靠体系了。

这就是生产团队做 React 单元测试的主要问题。写几个能通过的测试并不难,难的是构建一套在重构、版本发布、热修复和跨平台打包期间仍能保护你的测试套件。React 应用出问题,往往不是因为团队忘了怎么调用 render()。它们出问题,是因为测试逐渐偏向实现细节,异步行为被掩盖,而 CI 只把测试当成一个勾选项,而不是发布门禁。

现代 React 单元测试要像安全系统一样运作:本地快速反馈,CI 中做确定性检查,并清楚划分单元测试该覆盖什么、不该覆盖什么。当同一套 React 代码要发布到浏览器、Capacitor 容器或 Electron 外壳时,这一点更重要。

目录

为什么 React 单元测试是你最好的安全网

单元测试的价值,在于能抓住你原本确信不会发生的错误。在 React 中,这通常意味着组件仍能渲染,但用户依赖的行为已经变了。比如:禁用按钮变得可点击;加载状态一直不消失;重构后 fallback 消息消失。这些失败在 code 中影响很小,在生产环境却代价高昂。

React 测试发生重要变化,始于 React Testing Library 成为测试行为而非内部实现的主流模式,推动团队转向模拟用户行为而非组件 props 或 state 的测试,这也体现在 React Native 的测试指南中: React Native 测试概览。这种转变很重要,因为 React code 经常被重新调整。Hooks 会移动,组件会拆分,Context 会被引入。绑定内部结构的测试会在正常重构中失败;绑定可见行为的测试通常能存活下来。

单元测试应该保护什么

一个好的 React 单元测试只守护一个小契约:

  • 渲染输出: 用户是否看到了正确的文本、标签、状态或回退内容?
  • 交互行为: 点击、输入或切换时,UI 是否正确更新?
  • 边界处理: 当组件接收到预期输入、缺失数据或错误路径时,其行为是否正确?

薄弱的测试保护了错误的东西:

  • 组件内部实现: 状态结构、私有方法、仅用于实现的 props
  • 框架机制: React 是否以你预期的内部方式更新了 hook
  • 子组件细节: 这里不打算验证的、由嵌套组件负责的 Markup

实战规则: 如果你可以在不改变用户所见或操作的前提下重构组件,那么测试也不应该随之改变。

单元测试也属于更广泛的测试体系。它们并不试图证明整个 App 能端到端正常工作,而是在你需要浏览器级测试或设备级验证之前,作为快速的一层捕获回归。这就是为什么在任何合理的测试栈中,它们都是第一道防线: 生产级应用的自动化测试.

对于频繁发布的 React 团队来说,信心来自这种分工:单元测试快速捕获局部回归;集成测试验证衔接处;端到端测试确认关键路径。跳过单元测试层,下游所有更慢的环节都会承担过重的压力。

搭建现代 React 测试环境

脆弱的测试环境会在你写下第一条断言之前就制造出 flaky 测试。许多开发者会归咎于 Jest、jsdom 或 React,但根本原因往往是本地机器与 CI 之间的配置不一致。解决办法是让环境变得无聊。在这里,无聊是好事。

一个整洁的工作区,电脑显示器上显示着 code 编辑器中的 React 单元测试 code。

从可预测的 runner 和环境开始

对于现代 React 应用(尤其是使用 Vite 创建的应用),基础配置应包括:

  • 测试运行器: Jest 仍然很常见,尤其是在较旧的 React 代码库和企业 CI 技术栈中。
  • 类浏览器环境: jsdom 让组件测试能够渲染 DOM 输出。
  • Testing Library 工具: @testing-library/react 以及 @testing-library/jest-dom
  • 单一 setup 入口: 用一个文件注册 matchers 和全局 mocks

React 测试指南强调的核心工作流很简单:在 jsdom 环境中渲染组件,用类似下面的选择器查询 UI: getByText 或 getByRole,触发交互并断言 DOM 变化,正如 React 测试文档中所描述的那样。只有每台机器都运行相同的测试环境,这个工作流才可信。

一套实用的 Jest 配置通常如下:

// jest.config.js
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
  moduleNameMapper: {
    '\\.(css|less|scss)$': 'identity-obj-proxy',
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  transform: {
    '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
  },
};

如果你的团队使用 SWC 而不是 Babel,也没问题。关键不在转换器,而在于一致性。选定一种方案并在仓库中统一。如果你想要一份更广泛的 JavaScript 测试规范的配套参考,Capgo 的 JavaScript 单元测试指南 是一份很有用的团队交接文档。

添加测试套件所依赖的 setup 文件

一套完善的 setupTests.js 省去大量重复的噪音:

import '@testing-library/jest-dom';

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

在这个文件里,你可以一次性解决环境差异问题,而不是在二十个测试文件里分别处理。为 UI 依赖的 API 添加 mock,例如 matchMedia, ResizeObserver,或 IntersectionObserver,如果你的组件库需要它们的话。

如果没有这一步,开发者会临时修补全局变量。这会导致测试不一致,故障也难以追踪。有人本地能跑通,是因为他在某个文件里手动加了 mock;CI 却失败,因为配置没有共享。

让本地和 CI 行为保持一致

本地命令应尽量与 CI 命令保持一致。如果开发者用宽松的默认配置跑 watch 模式,而 CI 跑更严格的配置,合并后就会莫名失败。脚本要写明确:

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:ci": "jest --runInBand --coverage"
  }
}

简短的走查能帮新成员快速对齐基线:

对默认值保持克制,是最有影响的配置选择。把别名放进配置,把环境 mock 放进同一个 setup 文件。尽可能使用 jsdom 适合 UI 测试的环境,纯工具函数则用更轻量的环境。每个测试需要的自定义行为越少,系统就越可靠。

编写有意义的组件测试

企业的问题不在于写测试,而在于写出半年后仍然有意义的测试。

React 组件单元测试的标准模式仍然适用: 渲染组件,用面向用户的选择器查询 UI,触发交互,并断言由此产生的 DOM 变化,这能让测试远离 state 或 props 这类实现细节,正如 React 测试指南中所述。诀窍在于克制地应用这一模式。

像用户那样测试折叠面板

以一个基础 Accordion 组件为例。它渲染一个带标题的按钮。面板内容初始为隐藏。点击按钮会显示内容并更新无障碍状态。

这些行为足以编写几个实用的测试:

  1. 初始渲染显示标题但不显示内容。
  2. 点击触发器会显示内容。
  3. 再次点击则折叠内容。
  4. 无障碍属性反映可见状态。

最后一点经常被忽略。如果你的组件使用了 aria-expanded, aria-controls,或基于 role 的结构,请验证它们。这些不是实现细节,而是面向用户契约的一部分。

最好的组件测试读起来应该像一份你永远不想收到的 bug 报告。

根据意图选择查询方式

React Testing Library 提供了多种查询方式,但它们不能互换。选错方式会让测试变得嘈杂或产生误导。

查询类型 当找到元素时 当未找到元素时 使用场景示例
getBy 立即返回元素 立即抛出错误 断言按钮或标题应已显示在屏幕上
queryBy 立即返回元素 返回 null 断言交互前不应存在隐藏内容
findBy 元素出现时 resolve 等待后 reject 断言异步加载的内容在 fetch 或延迟更新后出现

一个简单的心智模型会有帮助:

  • 使用 getBy 用于必须已经存在的内容。
  • 使用 queryBy 用于必须尚不存在的内容。
  • 使用 findBy 用于 UI 随后发生变化的情况。

如果测试以以下内容开头: findBy 用于所有情况,通常意味着作者不确定组件何时更新。这种不确定性之后会变成 flaky。

一个实用的折叠面板示例

下面是一个有代表性的组件:

function Accordion({ title, children }) {
  const [open, setOpen] = React.useState(false);

  return (
    <section>
      <button
        aria-expanded={open}
        aria-controls="accordion-panel"
        onClick={() => setOpen(prev => !prev)}
      >
        {title}
      </button>
      {open ? (
        <div id="accordion-panel">
          {children}
        </div>
      ) : null}
    </section>
  );
}

以下是值得保留的测试形态:

import { render, screen, fireEvent } from '@testing-library/react';

test('renders the accordion title and hides content initially', () => {
  render(<Accordion title="Shipping details">Delivery takes 3 days</Accordion>);

  expect(screen.getByRole('button', { name: /shipping details/i })).toBeInTheDocument();
  expect(screen.queryByText(/delivery takes 3 days/i)).not.toBeInTheDocument();
});

test('reveals content when the trigger is clicked', () => {
  render(<Accordion title="Shipping details">Delivery takes 3 days</Accordion>);

  fireEvent.click(screen.getByRole('button', { name: /shipping details/i }));

  expect(screen.getByText(/delivery takes 3 days/i)).toBeInTheDocument();
});

test('updates aria-expanded when opened', () => {
  render(<Accordion title="Shipping details">Delivery takes 3 days</Accordion>);

  const button = screen.getByRole('button', { name: /shipping details/i });
  expect(button).toHaveAttribute('aria-expanded', 'false');

  fireEvent.click(button);

  expect(button).toHaveAttribute('aria-expanded', 'true');
});

缺失的部分同样重要。没有针对内部状态的断言。没有检查 setOpen 是否被调用。也没有整个渲染树的快照。这些测试只会增加维护成本,而非提升信心。

几个习惯能让组件测试更健壮:

  • 优先使用基于角色的查询: 按钮、标题、对话框、警报和输入框通常应通过角色来查找。
  • 保持每个测试范围狭窄: 每个测试只验证一个用户可见的行为,能让失败信息更易读。
  • 按结果命名测试: “打开时更新 aria-expanded” 比 “正常工作” 有用得多。

如果一个组件难以通过 DOM 进行测试,这往往暴露了设计问题。可能是状态隐藏在了错误的位置,也可能是缺乏语义化标记。好的测试往往能推动团队构建更好的组件。

测试自定义 Hooks 与应用逻辑

React 应用把大量重要行为藏在组件之外。状态转换在 hooks 里,校验和格式化在 helper 函数里,数据整形往往发生在任何渲染之前。如果只测试可见组件,就会漏掉很大一部分仍可能破坏生产行为的 code。

Hooks 需要能感知 React 的测试环境

自定义 hook 仍需要 React 才能正确执行,所以用 renderHook 并把会改变状态的调用包在 act().

一个简单的 useToggle hook 就是一个好例子:

import { useState, useCallback } from 'react';

export function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  const toggle = useCallback(() => setValue(current => !current), []);
  return { value, toggle };
}

它的测试应聚焦于公共契约:

import { renderHook, act } from '@testing-library/react';
import { useToggle } from './useToggle';

test('returns the initial value', () => {
  const { result } = renderHook(() => useToggle(true));
  expect(result.current.value).toBe(true);
});

test('toggles the value', () => {
  const { result } = renderHook(() => useToggle(false));

  act(() => {
    result.current.toggle();
  });

  expect(result.current.value).toBe(true);
});

这个测试有用,是因为 hook 本身就是单元。你不是在测 React 内部,而是在验证 hook 的外部行为。

对于构建可复用 UI 或功能基础组件的产品团队来说,这种模式非常重要。Hooks 往往会成为跨应用、设计系统或内部工具共享的接口。如果你正带着商业目的设计可复用行为,那么关于 面向创客产品的 hooks 资源 可以帮助把 hooks 看作产品化构建模块,而不只是实现细节。

纯逻辑在测试中应保持纯粹

并非所有东西都需要 jsdom、React 或 Testing Library。如果函数是纯函数,直接在 Node 环境里用 Jest 测就行。

示例:

export function formatDisplayName(firstName: string, lastName: string) {
  return `${firstName.trim()} ${lastName.trim()}`.trim();
}

这种测试就该简单到不能再简单:

import { formatDisplayName } from './formatDisplayName';

test('joins and trims both names', () => {
  expect(formatDisplayName(' Ada ', ' Lovelace ')).toBe('Ada Lovelace');
});

test('handles a missing last name', () => {
  expect(formatDisplayName('Ada', '')).toBe('Ada');
});

这样做的好处是速度快、逻辑清晰。函数不需要渲染树时,就别给它渲染树。React 专用工具会带来额外开销。业务逻辑测试要保持小而快,并贴近它要验证的函数。

一种实用的拆分方案如下:

  • Hooks: 使用 renderHook, act(),需要时再加上 wrapper providers。
  • 工具函数: 直接用 Jest,不要 DOM。
  • 有状态的横切逻辑: 当组件测试变得过于臃肿时,将其拆分为可测试的辅助函数。

团队常常在组件测试中塞入本应属于下层堆栈的逻辑断言。将这些逻辑抽离出来有两个好处:组件测试更清晰,逻辑测试更快。

进阶技巧:掌握 Mock 与异步测试

大多数不稳定的 React 测试套件会在两个地方出错:依赖边界和时间相关逻辑。

这就是为什么异步测试和 Mock 是区分玩具级测试套件与发布前可信赖测试套件的分水岭。一项分析将 46.5% 的测试不稳定归因于环境或资源相关问题,例如异步时序 中的 这份 React 单元测试分析。在 React 应用中,这直接对应于状态转换、延迟渲染、网络驱动的 UI,以及那些靠猜测而非确定性等待的测试。

一张对比图表,展示高级 React 测试技术,重点聚焦于依赖项 Mock 与异步测试。

Mock 边界,而非每一层

写误导性测试最快的方式,就是 mock 掉一半组件树,然后断言自己的 mock 生效了。

对于获取账户数据的组件,mock 网络客户端或 API 模块。除非测试确实需要在这些环节做隔离,否则不要 mock hook、子行组件、loading spinner 和三个工具函数。

请遵循以下规则集:

  • Mock 外部服务: HTTP 客户端、analytics、仅浏览器可用的 API、原生桥接
  • Mock 不稳定的平台 API: matchMedia、定时器、Electron preload 接口,以及在 jsdom 中不可用的 Capacitor 插件
  • 默认不要 mock 自己的内部实现: 自定义 hooks、简单子组件、本地工具函数

如果一个测试通过,只是因为所有难搞的部分都被 fake 替换了,那它并不能给你带来多少发布信心。

对于想了解 runner API 示例和模式的团队,Capgo 测试教程 是一个实用的参考库,尤其适合 onboarding 懂 React 但还不熟悉测试机制的开发者。

异步测试在时序模糊时易失败

异步失败通常来自以下三种错误之一:

  1. 测试断言得太早。
  2. 测试用任意定时器等待。
  3. 组件更新了不止一次,但测试只模拟了一次状态转换。

稳定的异步测试通常是这样:

test('shows user details after data loads', async () => {
  render(<UserProfile userId="42" />);
  expect(screen.getByText(/loading/i)).toBeInTheDocument();

  expect(await screen.findByText(/account owner/i)).toBeInTheDocument();
});

或者,当你需要等待特定条件时:

await waitFor(() => {
  expect(screen.getByRole('alert')).toBeInTheDocument();
});

使用 findBy 当某个元素出现就是你关心的事件时,使用 waitFor 当条件更宽泛,或状态无法用单个查询表达时,避免 setTimeout 在测试中使用,除非你明确在测试定时器行为并使用 fake timers。

React 的测试生态也要求你遵守 act() 更新相关的语义。Testing Library 帮你处理了大部分情况,但如果你手动驱动状态或推进计时器,仍需考虑更新何时刷新。

知道该用哪种 Mock 工具

不同的 mock 工具解决不同的问题:

工具 最佳用途 常见错误
jest.fn() 独立的假回调或注入函数 当简单回调就足够时,用它来替换整个模块
jest.spyOn() 观察或覆盖真实对象或模块上的单个方法 忘记恢复原始实现
jest.mock() 在导入边界处替换模块依赖 默认 Mock 大型模块,导致丢失有意义的行为

示例很有帮助:

  • 尝试使用 jest.fn() 当组件接收一个 onSubmit prop 时。
  • 使用 jest.spyOn() 当你需要验证 console.error、一个存储方法,或一次导出的 API 调用时。
  • 使用 jest.mock() 当导入模块会触发 I/O、原生 code,或触及单元边界之外的行为时。

许多指南对现代 React 的错误路径测试关注不足。错误边界、延迟的状态变更和异步回退 UI 都值得认真测试,而不只是“快乐路径”的点击示例。如果子组件抛出错误,就断言回退 UI;如果请求失败,就断言可见的恢复状态;如果按钮在加载期间被禁用,也要断言。这些才是用户真正记得的 Bug。

提升测试质量与策略

很多团队依然把覆盖率等同于信心,但事实并非如此。

即使达到了覆盖率目标,你仍可能错过关键的回归问题。一个充斥着浅层断言、宽泛的 snapshot 以及大量 mock 内部实现的测试套件,看似提供了安全保障,实则增加了维护成本。

一张信息图,对比高质量测试的收益与高数量测试套件的维护开销。

覆盖率是地图,而非目标

只有当覆盖率报告能回答一个问题时,它才真正有用:哪些关键路径目前还没有保护?

如果报告只是逼着开发者去测试琐碎的 wrapper、静态 markup 或只有一行的透传文件,只为把百分比推上去,那它就没用。把覆盖率当作发现工具。如果认证状态、计费操作、feature flags 或更新提示没有测试,这是一个信号;如果只是一个展示型 icon 组件没有测试,通常不算。

一个有效的评审问题很简单:这个测试能降低发布风险吗?

  • 是: 它验证关键路径上用户可见的行为。
  • 可能: 它保护的是重构时容易被破坏的业务逻辑。
  • 否: 它断言实现细节,或者重复另一个测试的价值。

哪些不该做单元测试

许多 React 指南仍没花足够篇幅讲“不该测什么”。这个缺口很关键:过度 mock 和测试实现细节会造出脆弱的测试套件——测试通过了,用户体验却照样出问题,正如 BrowserStack 关于 《React 中哪些内容不应做单元测试》的指南所述。.

跳过或严格限制这些模式:

  • 内部状态断言: 不要直接测试 isOpen ,而是测试面板是否打开。
  • 框架行为: 不要测试 React 是否调用了某个 effect。要测试这个 effect 带来的结果。
  • 第三方库内部实现: 测试你与日期选择器或路由器的集成,而不是库自身的渲染逻辑。
  • 过度拆分的单元: 如果你 mock 了所有子组件和辅助函数,可能就没在测试有意义的行为。

糟糕的测试比缺失的测试更糟,因为它们会阻碍重构,却依然抓不到生产环境的 bug。

一个实用的启发式原则是边界所有权。测试你的 code 负责的部分。除非你的集成层改变了契约,否则不要测试 React、浏览器或成熟库已经负责的部分。

Snapshot 测试的利与弊

快照并非毫无用处,只是很容易被误用。

仅对输出稳定且简单的组件谨慎使用,此时宽泛的结构 diff 才有意义。避免用于交互式或高度动态的组件,因为它们会变成噪音。开发者会停止阅读它们,并开始习惯性地更新它们。

通常存在更好的替代方案:

  • 对于条件渲染,断言关键文本的存在或不存在。
  • 对于视觉状态变化,断言重要的 role、label 或 attribute。
  • 对于错误和回退,断言实际的消息或 alert 区域。

如果你的团队需要超越单元测试的更广泛质量流程,一个可靠的伴侣是 应用质量保证工作流 把测试、发布检查和回滚计划视为一个系统。这种思维转变能最快提升测试质量。别再问自己有多少测试,而要开始问:哪些故障仍可能影响到用户。

将测试集成到跨平台 CI/CD Pipeline

只在开发者笔记本上运行的测试套件,只是建议,不是管控。

只有当每个 pull request 都在干净环境中运行相同检查,并在检查失败时阻止合并,测试套件才真正运转起来。这听起来理所当然,但很多团队仍留下关键缺口:测试靠手动跑、覆盖率报告可有可无、打包和发布任务在测试任务完成前就启动。小 UI 回归就是这样演变成更大的发布故障。

一张五步流程图,展示如何将 React 自动化测试集成到 CI/CD 开发 pipeline 中。

每次 pull request 都应触发同一个门禁

要让 React 单元测试真正起到安全网的作用,CI 需要具备几个关键要素:

  • 每次 pull request 都运行
  • 根据 lockfile 安装依赖
  • 每次都用同一个测试命令
  • 测试失败时立即中断
  • 仅在测试通过后发布 artifacts

这正是 应用团队持续部署实践的核心。要在发布前建立信心,而不是发布后。

对于许多团队来说,一个简单的 GitHub Actions 工作流就足够了:

name: test

on:
  pull_request:
  push:
    branches:
      - main

jobs:
  react-tests:
    runs-on: ubuntu-latest

    steps:
      - name: Check out code
        uses: actions/checkout@v4

      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Run unit tests
        run: npm run test:ci

这并不花哨,而这正是关键。最强的 pipeline 往往也是最不令人意外的。

为什么这对 Capacitor 和 Electron 更重要

跨平台 React 应用的发布风险高于纯浏览器应用,因为同一套 UI code 往往会打包进不同的容器,并带有不同的运行时假设。

以下几个例子说明 pipeline 在哪些地方有帮助:

  • Capacitor 应用: Web code 可能在本地通过,但打包后,插件 bridge、离线状态或应用生命周期边界情况改变行为时就会失败。
  • Electron 应用: 渲染进程组件可能依赖 preload API、窗口消息传递或仅桌面端才有的状态,而在纯浏览器测试中,除非刻意 mock,这些都不存在。
  • 共享发布列车: 如果部署流程未严格限制发布,一个有问题的 bundle 可能会影响多个目标。

因此,单元测试应在打包任务之前运行,打包任务应在分发任务之前运行。每个阶段都在降低风险。单元测试能快速捕获本地回归问题。平台打包验证环境假设。手动审批或分阶段发布处理最终发布信心。

一个实用的 GitHub Actions 工作流

更成熟的 pipeline 通常会拆分职责:

  1. 测试任务: 快速的单元测试和 hook 测试
  2. 构建任务: 仅在测试通过后执行生产构建
  3. 打包任务: Capacitor 同步、Electron 打包或 artifact 捆绑
  4. 发布任务: 仅从已批准的分支或标签发布

对于向 Capacitor 或 Electron 应用发布 Live Updates 的团队来说,这正是发布工具发挥作用的地方。该工作流中的一个选项是 Capgo,它能为 CapacitorJS 和 Electron 应用发布带有回滚支持和基于渠道的发布控制的已签名 web bundle。在实践中,这意味着你的 React 测试任务可以在任何 web bundle 晋升到 staging 或生产环境之前,充当第一道硬性关卡。

操作原则很简单:不要让发布基础设施去弥补测试的不足。应该在可靠的测试已经过滤掉错误变更之后,再使用发布基础设施。

可靠的测试系统会改变团队行为。工程师合并代码时不再犹豫;审查者可以专注于边缘情况,而不是手动重复基础测试;发布经理也不再把每次部署都当成一场豪赌。这就是做好 React 单元测试带来的成果。


如果你的团队通过 Capacitor 或 Electron 发布 React,那么发布安全性不仅仅取决于本地测试是否通过。 Capgo 让团队能以受控方式发布已签名的 web 更新、面向指定的发布渠道,并回滚有问题的 bundle,而无需等待应用商店审核;这能自然地接在部署前已要求单元测试通过的 CI pipeline 之后。

为 Capacitor 应用提供 Live Updates

当 Web 层出现线上 Bug 时,通过 Capgo 发布修复,而不用等 App Store 审核好几天。用户会在后台收到更新,原生变更则仍走正常审核流程。

博客最新动态

Capgo 为你提供打造真正专业的移动应用所需的最有价值的洞察。