Chapter 09

测试与发布

Jest 单元测试、Detox E2E 测试,EAS Build 云端构建,OTA 热更新,App Store / Google Play 上架全流程。

移动端测试的特殊挑战

移动端测试比 Web 开发更复杂,原因是多方面的:设备碎片化(Android 有数千种设备,iOS 有不同屏幕尺寸)、操作系统版本差异、权限管理(相机、位置、推送等需要用户授权)、网络状态变化、后台/前台切换行为等,都是 Web 开发几乎不需要面对的挑战。

因此移动端测试策略通常分层:单元测试覆盖业务逻辑(快速、稳定);集成测试覆盖组件交互;E2E 测试覆盖关键用户流程(慢但最真实)。

核心名词解释

Jest
Facebook 开发的 JavaScript 测试框架,React Native 内置集成。提供测试运行器、断言库、Mock 功能、快照测试。是 RN 单元测试和集成测试的标准选择。
@testing-library/react-native
组件测试库,提供 render(渲染组件)、screen(查询 DOM)、fireEvent(模拟用户操作)等 API。哲学:测试组件的行为而非实现细节。
Detox
Wix 开源的 React Native E2E 测试框架,"灰盒测试"方案——测试代码运行在 Node.js,但直接操控真实模拟器/设备上的原生 UI,比纯黑盒测试更稳定。
EAS Build
Expo 的云端构建服务。在 Expo 的服务器上运行 Xcode 和 Android SDK,生成 .ipa 和 .apk/.aab 文件,你无需本地安装这些工具。对 CI/CD 非常友好。
EAS Submit
Expo 的自动提交服务,配合 EAS Build 构建完成后自动上传到 App Store Connect 或 Google Play Console,省去手动上传步骤。
TestFlight
Apple 官方内部测试平台,上传 .ipa 后可邀请测试人员安装(无需上架 App Store)。内部测试最多 100 人,外部测试最多 10000 人。
OTA Update(Over The Air)
热更新,通过 expo-updates 将新的 JS Bundle 推送到用户设备,无需提交 App Store 审核。适合紧急 Bug 修复和小功能更新。注意:纯原生代码变更不能 OTA,必须走完整发布流程。

发布流程总览

React Native 应用发布流程 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 代码变更 │ ▼ Jest 单元测试(PR 时自动运行) │ 失败 → 修复 ▼ Detox E2E 测试(关键流程验证) │ ▼ eas build --profile preview ← 内部测试构建 │ ▼ TestFlight / Google 内部测试 ← 邀请测试人员 │ 发现 Bug → 修复 ▼ eas build --profile production ← 生产构建 │ ▼ eas submit ← 自动提交审核 │ ├── App Store 审核:1-3 天 │ └── Google Play 审核:2-7 天 │ ▼ 正式上线 🚀 │ ▼ 小 Bug 修复 → expo-updates OTA ← 无需重新审核!

单元测试:Jest + Testing Library

// components/__tests__/LikeButton.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react-native';
import { LikeButton } from '../LikeButton';

// Mock React Query 的 useMutation
const mockMutate = jest.fn();
jest.mock('@tanstack/react-query', () => ({
  ...jest.requireActual('@tanstack/react-query'),
  useMutation: () => ({ mutate: mockMutate, isPending: false }),
}));

describe('LikeButton', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('显示初始点赞数', () => {
    render(<LikeButton postId="p1" initialLikes={42} liked={false} />);
    expect(screen.getByText('42')).toBeTruthy();
  });

  it('点击后调用 like API', async () => {
    render(<LikeButton postId="p1" initialLikes={10} liked={false} />);
    const button = screen.getByRole('button', { name: /点赞/ });
    fireEvent.press(button);

    await waitFor(() => {
      expect(mockMutate).toHaveBeenCalledWith('p1');
    });
  });

  it('乐观更新:点击后立即显示 +1', () => {
    render(<LikeButton postId="p1" initialLikes={10} liked={false} />);
    fireEvent.press(screen.getByRole('button'));
    // 乐观更新,不等待网络,立即显示 11
    expect(screen.getByText('11')).toBeTruthy();
  });

  it('快照测试', () => {
    const { toJSON } = render(
      <LikeButton postId="p1" initialLikes={5} liked={true} />
    );
    expect(toJSON()).toMatchSnapshot();
  });
});

EAS Build 配置

// eas.json — EAS Build 配置文件
{
  "cli": {
    "version": ">= 7.0.0"
  },
  "build": {
    "development": {
      "developmentClient": true,       // Development Build(支持自定义原生模块)
      "distribution": "internal",
      "ios": { "simulator": true }    // 支持模拟器
    },
    "preview": {
      "distribution": "internal",    // 内部分发(TestFlight / 内部测试)
      "channel": "preview"
    },
    "production": {
      "distribution": "store",       // 应用商店分发
      "channel": "production",
      "ios": {
        "buildConfiguration": "Release"
      },
      "android": {
        "buildType": "app-bundle"   // AAB 格式(Google Play 要求)
      }
    }
  },
  "submit": {
    "production": {
      "ios": {
        "appleId": "your@apple.com",
        "ascAppId": "1234567890"
      },
      "android": {
        "serviceAccountKeyPath": "./google-service-account.json",
        "track": "internal"        // 先发内部测试轨道
      }
    }
  }
}

OTA 热更新配置

// app.json — expo-updates 配置
{
  "expo": {
    "updates": {
      "url": "https://u.expo.dev/your-project-id",
      "checkAutomatically": "ON_LOAD"  // 启动时检查更新
    },
    "runtimeVersion": {
      "policy": "sdkVersion"  // 根据 SDK 版本决定兼容性
    }
  }
}

// hooks/useOTA.ts — 手动检查并应用更新
import * as Updates from 'expo-updates';

export function useOTAUpdate() {
  useEffect(() => {
    async function checkUpdate() {
      try {
        const update = await Updates.checkForUpdateAsync();
        if (update.isAvailable) {
          await Updates.fetchUpdateAsync();
          // 提示用户重启以应用更新
          Alert.alert('发现新版本', '重启应用以应用更新', [
            { text: '稍后', style: 'cancel' },
            {
              text: '立即重启',
              onPress: () => Updates.reloadAsync(),
            },
          ]);
        }
      } catch (e) {
        console.log('OTA 检查失败', e);
      }
    }
    checkUpdate();
  }, []);
}
审核周期提醒 App Store 通常需要 1-3 个工作日审核(有时更快),Google Play 首次上架通常需要 2-7 天。版本发布要提前计划,不能临时抱佛脚。重大发布建议在节假日前至少两周提交审核,避免假期期间审核队伍减员导致延误。
灰度发布 App Store 支持分阶段发布(Phased Release),可以先向 1% / 2% / 5% / 10% / 20% / 50% / 100% 的用户逐步推送。Google Play 支持按百分比的正式发布轨道。利用灰度发布可以在大规模推广前发现崩溃和用户反馈问题。