CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Flutter 开发入门/课时/测试
RawBlame
课时目录12 节

Flutter 开发入门

  1. 01Flutter 环境
  2. 02Dart 语言
  3. 03Widget 体系
  4. 04架构原则
  5. 05状态管理
  6. 06动画
  7. 07网络与本地存储
  8. 08Firebase 集成
  9. 09测试
  10. 10CI/CD 与发布
  11. 11分析与监控
  12. 12综合练习

测试

Unit/Widget/Integration。

30 分钟

测试

这一课你将能做到

  1. 写单元测试(Unit Test)验证纯 Dart 业务逻辑
  2. 写 Widget 测试验证 UI 组件的渲染和交互
  3. 用 mockito 或 mocktail 做依赖 Mock,写集成测试(Integration Test)跑完整流程

为什么要学

Flutter 的测试金字塔:Unit 测试最多最快,Widget 测试居中,Integration 测试最少最慢但最接近真实。不写测试,改代码就是开盲盒。

人话:Unit 测试是「检查零件」,Widget 测试是「检查组装」,Integration 测试是「上路试驾」。三层配合,bug 无处藏身。

先建立图像

        /\
       /  \      Integration Test(少、慢、全链路)
      /____\
     /      \    Widget Test(中、较快、单组件)
    /________\
   /          \  Unit Test(多、快、纯逻辑)
  /____________\

跟着做

步骤 1:添加测试依赖

pubspec.yaml:

dev_dependencies:
  flutter_test:
    sdk: flutter
  mocktail: ^1.0.0
  integration_test:
    sdk: flutter

步骤 2:单元测试

// lib/calculator.dart
class Calculator {
  int add(int a, int b) => a + b;
  int divide(int a, int b) {
    if (b == 0) throw ArgumentError('除数不能为 0');
    return a ~/ b;
  }
}

// test/calculator_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:myapp/calculator.dart';

void main() {
  group('Calculator', () {
    final calc = Calculator();

    test('add 正常', () {
      expect(calc.add(2, 3), 5);
    });

    test('divide 正常', () {
      expect(calc.divide(10, 2), 5);
    });

    test('divide 除 0 抛异常', () {
      expect(() => calc.divide(1, 0), throwsArgumentError);
    });
  });
}

运行:

flutter test

步骤 3:Widget 测试

// lib/counter_page.dart
class CounterPage extends StatefulWidget {
  const CounterPage({super.key});
  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _count = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(child: Text('$_count')),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() => _count++),
        child: const Icon(Icons.add),
      ),
    );
  }
}

// test/counter_page_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:myapp/counter_page.dart';

void main() {
  testWidgets('点击按钮计数器增加', (tester) async {
    await tester.pumpWidget(const MaterialApp(home: CounterPage()));

    expect(find.text('0'), findsOneWidget);

    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();          // 触发 rebuild

    expect(find.text('1'), findsOneWidget);
  });
}

关键 API:

  • tester.pumpWidget(widget):渲染 Widget
  • tester.tap(finder):模拟点击
  • tester.pump():触发一帧 rebuild
  • tester.pumpAndSettle():等所有动画结束
  • find.text('x') / find.byIcon(...) / find.byKey(...):查找 Widget

步骤 4:Mock 网络请求

// lib/todo_repository.dart
abstract class TodoRepository {
  Future<List<String>> fetchTodos();
}

// test/todo_page_test.dart
import 'package:mocktail/mocktail.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:myapp/todo_repository.dart';

class MockTodoRepository extends Mock implements TodoRepository {}

void main() {
  testWidgets('加载待办列表', (tester) async {
    final repo = MockTodoRepository();
    when(() => repo.fetchTodos()).thenAnswer((_) async => ['买牛奶', '写代码']);

    await tester.pumpWidget(MaterialApp(home: TodoPage(repository: repo)));
    await tester.pumpAndSettle();

    expect(find.text('买牛奶'), findsOneWidget);
    expect(find.text('写代码'), findsOneWidget);
  });
}

mocktail 核心:when(() => mock.method()).thenAnswer(...),验证用 verify(() => mock.method()).called(1)。

步骤 5:集成测试

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

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('完整流程:登录 → 添加待办', (tester) async {
    app.main();
    await tester.pumpAndSettle();

    // 输入邮箱密码
    await tester.enterText(find.byKey(const Key('email')), 'a@b.com');
    await tester.enterText(find.byKey(const Key('password')), '123456');
    await tester.tap(find.text('登录'));
    await tester.pumpAndSettle();

    // 添加待办
    await tester.tap(find.byIcon(Icons.add));
    await tester.pumpAndSettle();
    await tester.enterText(find.byKey(const Key('title')), '测试任务');
    await tester.tap(find.text('保存'));
    await tester.pumpAndSettle();

    expect(find.text('测试任务'), findsOneWidget);
  });
}

运行:

flutter test integration_test

集成测试跑在真机/模拟器上,速度很慢(分钟级),只覆盖核心链路。

步骤 6:Golden 测试(视觉回归)

testWidgets('CounterPage 视觉一致', (tester) async {
  await tester.pumpWidget(const MaterialApp(home: CounterPage()));
  await expectLater(
    find.byType(CounterPage),
    matchesGoldenFile('counter_page.png'),
  );
});

第一次运行生成基准图:

flutter test --update-goldens

之后每次跑测试都会对比像素差异,防止 UI 被意外改坏。

讲清楚要点

  1. 测试金字塔:Unit(多快)→ Widget(中)→ Integration(少慢),比例约 70/20/10。
  2. Widget 测试不跑真机:在内存里渲染,速度接近 Unit 测试,能验证布局、交互、状态。
  3. Mock 是为了隔离依赖:网络、数据库、平台通道都应该 Mock,保证测试确定性。
  4. Integration 测试覆盖「核心用户路径」:注册→登录→核心功能→退出,不要追求 100% 覆盖。
  5. Golden 测试防 UI 回归:适合设计系统组件,不适合频繁变动的业务页面。

你可能会懵的地方

  • tester.pump() 和 pumpAndSettle() 区别? pump() 只触发一帧;pumpAndSettle() 循环触发直到没有动画/定时器,等 UI 完全稳定。

  • Widget 测试能跑网络请求吗? 能,但不应该。网络请求应该 Mock,否则测试慢且不稳定。

  • mockito 和 mocktail 选哪个? mocktail 不需要代码生成,API 更简洁;mockito 需要 build_runner 生成 mock 类。新项目推荐 mocktail。

  • 为什么 find.text('0') 找不到? 文本可能不在 Text Widget 里(比如 RichText);或者 Widget 还没渲染(需要 pump());或者文本被拆成多个 TextSpan。

  • 集成测试怎么调试? 加 --dart-define 传环境变量;用 tester.takeScreenshot() 截图;在关键步骤加 debugPrint。

错法对照

错法问题正解
Widget 测试里写 await Future.delayed(...)测试慢且不稳定用 tester.pump(Duration(...)) 快进时间
不 Mock 直接发真实网络请求测试慢、依赖网络用 mocktail Mock Repository
expect(find.text('x'), findsOneWidget) 前没 pump()Widget 还没 rebuildtap 之后必须 pump() 或 pumpAndSettle()
Golden 测试在 CI 上跑不同设备渲染像素不同CI 上禁用 Golden,或固定设备/字体
集成测试里测边界条件慢且难维护边界条件放 Unit/Widget 测试,集成测试只跑主流程

卡点急救

  1. pumpWidget 报 No MediaQuery widget found:顶层没包 MaterialApp,加上即可。
  2. find.byKey 找不到:确认 Key 类型一致(Key('x') vs ValueKey('x'));确认 Widget 已渲染(pump())。
  3. Mock 不生效:when 的参数必须和实际调用完全匹配;用 any() 匹配任意参数。
  4. Golden 测试本地过 CI 挂:字体渲染差异。在 flutter_test_config.dart 里加载固定字体。
  5. 集成测试报 Target of URI doesn't exist:integration_test 包路径写错,确认 dev_dependencies 里有 integration_test: sdk: flutter。

面试常问

  • Flutter 测试分几层?各自特点? Unit:纯 Dart,最快,不依赖 Flutter 引擎;Widget:内存渲染,验证 UI 和交互;Integration:真机/模拟器,最慢但最真实。

  • Widget 测试的原理? testWidgets 创建一个 WidgetTester,内部用 TestWidgetsFlutterBinding 在内存里构建 Widget 树并渲染,不经过 Skia 绘制到屏幕。

  • 怎么测异步代码? Unit 测试直接 await;Widget 测试用 tester.pumpAndSettle() 等所有 Future 完成;Integration 测试同样用 pumpAndSettle。

  • 什么是 Golden 测试? 把 Widget 渲染成图片,和基准图做像素对比。用于设计系统组件的视觉回归测试。

自测三问

  1. 写一个 Widget 测试:验证点击按钮后文本从 "0" 变成 "1"。
  2. mocktail 的 when 和 verify 分别干什么?
  3. 为什么 Integration 测试不应该追求高覆盖率?

小结一句话

Flutter 测试 = Unit(快)+ Widget(中)+ Integration(慢);Mock 隔离依赖,Golden 防 UI 回归,三层配合保质量。

延伸

  • 官方测试文档:<https://docs.flutter.dev/testing>
  • mocktail 文档:<https://pub.dev/packages/mocktail>
  • 下一课:CI/CD 与发布,把测试跑在云端,自动打包上架。
加载本节练习…
← 上一课Firebase 集成下一课 →CI/CD 与发布