质量与交付篇(1/6):单元测试、Widget 测试、集成测试怎么分工

6 阅读5分钟

单元测试、Widget 测试、集成测试怎么分工:Flutter 项目测试体系落地指南

Flutter 测试 单元测试 Widget测试 集成测试 工程化
适用场景:业务迭代快、多人协作、需要降低回归成本的中大型项目

很多团队一提测试就两个极端:

  • 要么只写少量单测,UI 回归靠人点;
  • 要么想“一步到位全自动”,最后维护成本爆炸。

真正可长期执行的方案,不是“写更多测试”,而是先把三类测试分工清楚:谁兜底业务逻辑、谁兜底界面行为、谁兜底端到端主链路。


1. 问题背景:业务场景 + 现象

在 Flutter 业务项目里,常见现象是:

  • 发版前靠 QA + 开发手点回归,耗时长且容易漏边角流程;
  • 某次重构后,金额计算、状态流转这种逻辑悄悄回归;
  • 页面能打开,但按钮可点性、加载态、错误态展示不稳定;
  • 登录-下单-支付这种核心链路,线上偶发失败,测试环境却“测不出来”。

表面是“测试覆盖率不高”,本质是:没有明确“哪一类问题由哪一类测试负责”


2. 原因分析:核心原理 + 排查过程

核心原理:三类测试关注点不同

  • 单元测试(Unit Test):验证纯逻辑与状态流转,快、稳定、定位精准。
  • Widget 测试(Widget Test):验证组件树与交互反馈,成本适中。
  • 集成测试(Integration Test):验证端到端流程,最接近真实用户但最慢、最脆弱。

如果不分工,常见后果是:

  • 把纯逻辑塞进集成测试,跑得慢、失败难排查;
  • 把端到端风险寄希望于单测,线上出事故;
  • Widget 测试写成“快照式大而全”,维护困难。

快速排查(团队自检)

  1. PR 合并前,是否只跑了 flutter test,没有主链路集成测试?
  2. 业务规则(价格、权限、状态机)是否仍主要依赖手工验证?
  3. 页面改动后,是否缺少可重复执行的交互断言(按钮禁用、文案、loading/error)?
  4. 测试失败时,是否经常不知道该找业务同学、客户端同学还是环境问题?

中 2 条以上,说明测试分工需要重建。


3. 解决方案:方案对比 + 最终选择

方案对比

  • 方案 A:重集成、轻单测

    • 优点:看起来“覆盖真实流程”。
    • 缺点:运行慢、波动大、定位困难,团队很快放弃。
  • 方案 B:重单测、几乎无集成

    • 优点:执行快,开发体验好。
    • 缺点:跨页面、跨模块、原生互通问题兜不住。
  • 方案 C:分层金字塔(推荐)

    • 单测做大头,Widget 测试补 UI 行为,集成测试只盯核心链路。
    • 目标:快反馈 + 关键路径兜底

最终分工建议(可直接落地)

  • 单元测试(约 60%~75%)

    • 覆盖:UseCase、ViewModel/Notifier、数据转换、状态机、错误映射。
    • 不做:真实网络、真实数据库、真实平台通道。
  • Widget 测试(约 20%~30%)

    • 覆盖:组件可见性、点击交互、表单校验、加载/空态/错误态切换。
    • 不做:完整业务闭环、第三方 SDK 真调用。
  • 集成测试(约 5%~10%)

    • 覆盖:登录、核心支付前链路、核心房间进出、关键埋点触发。
    • 不做:所有分支全覆盖(会拖垮效率)。

4. 关键代码:最小必要代码片段

4.1 单元测试:验证状态流转(快且稳定)

import 'package:flutter_test/flutter_test.dart';

void main() {
  test('下单成功后状态应从 loading -> success', () async {
    final repo = FakeOrderRepo(success: true);
    final vm = OrderViewModel(repo);

    expect(vm.state, OrderState.idle);

    final future = vm.submitOrder();
    expect(vm.state, OrderState.loading);

    await future;
    expect(vm.state, OrderState.success);
  });
}

4.2 Widget 测试:验证交互与界面反馈

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';

void main() {
  testWidgets('点击提交后显示 loading,再显示成功文案', (tester) async {
    await tester.pumpWidget(const MaterialApp(home: OrderPage()));

    await tester.tap(find.text('提交'));
    await tester.pump(); // 触发首帧状态变化
    expect(find.byType(CircularProgressIndicator), findsOneWidget);

    await tester.pumpAndSettle();
    expect(find.text('提交成功'), findsOneWidget);
  });
}

4.3 集成测试:只保核心链路

import 'package:integration_test/integration_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('核心链路:登录 -> 进入首页 -> 发起下单', (tester) async {
    app.main();
    await tester.pumpAndSettle();

    await tester.enterText(find.byKey(const Key('phone_input')), '13800000000');
    await tester.tap(find.byKey(const Key('login_btn')));
    await tester.pumpAndSettle();

    expect(find.text('首页'), findsOneWidget);

    await tester.tap(find.byKey(const Key('create_order_btn')));
    await tester.pumpAndSettle();

    expect(find.text('下单成功'), findsOneWidget);
  });
}

5. 效果验证:数据/截图/日志

建议用 4 个指标验证分工是否有效:

  • 反馈速度:本地 flutter test 控制在 2~5 分钟内,开发愿意频繁跑。
  • 稳定性:集成测试 flaky rate(偶发失败率)持续下降。
  • 线上质量:回归类线上 bug(尤其是状态流转/UI 反馈)明显减少。
  • 定位效率:失败后能快速归因:逻辑问题看单测、UI 行为看 Widget、链路问题看集成。

可在 CI 中拆三段日志:

  1. unit_test:最快失败、先挡住明显逻辑回归;
  2. widget_test:校验交互行为;
  3. integration_smoke:仅跑冒烟主链路,避免流水线超时。

6. 可复用结论:通用经验 + 避坑清单

通用经验

  • 先定义测试职责边界,再谈覆盖率目标。
  • 单测优先覆盖“经常改、改了容易出事故”的规则。
  • Widget 测试重点放在“用户可感知行为”,不是拼命堆快照。
  • 集成测试要克制:只守核心路径,不追求全功能自动化。
  • 让测试成为 CI 的“准入门槛”,而不是可选项。

避坑清单

  • 把网络/SDK 真调用放进单元测试,导致慢且不稳定。
  • Widget 测试依赖复杂全局状态,构建成本过高。
  • 集成测试覆盖过宽,维护成本高于收益。
  • 没有统一测试数据工厂(Test Data Builder),样例重复难维护。
  • 只有“测试数量”,没有“失败率、耗时、拦截缺陷数”指标。

结语

三类测试不是竞争关系,而是协作关系:
单元测试保逻辑、Widget 测试保交互、集成测试保主链路
分工清晰后,你会发现测试不是负担,而是交付速度和线上稳定性的放大器。