L
← 返回技术文章
约 7 分钟

Flutter 单测问题

从 Unit Test、Widget Test 到 Integration Test,梳理 Flutter 测试分层、平台能力边界以及测试与代码设计的关系。

Flutter单元测试Widget TestTDD

Flutter 单测可能包含的移动端特性

核心区别在于面向的对象不同

后端单测主要验证:

数据和业务逻辑是否符合预期。

模型:

输入
 ↓
函数 / Service
 ↓
输出

例如:

输入订单金额和用户等级:

100元
VIP用户

↓

计算优惠

↓

80元

测试关注的是计算逻辑是否正确。


而前端单测除了验证逻辑,还需要验证:

状态变化之后,界面是否符合预期。

模型:

用户操作
 ↓
状态变化
 ↓
重新渲染
 ↓
界面变化

比如:

Given:

用户未登录


When:

点击收藏按钮


Then:

显示登录弹窗

这个过程不是直接调用某个状态修改函数,而是模拟真实用户行为:

用户点击

↓

组件响应

↓

状态变化

↓

重新build

↓

检查UI结果

类似于把组件放进一个假的运行环境中,模拟点击、输入、滑动等行为。

这一部分严格来说已经从 Unit Test 逐渐进入了 Component Test / Widget Test 的范围。

不过实际开发交流中,很多人会比较宽泛地把:

  • 不启动完整App
  • 不依赖真实后端
  • 执行速度较快

的测试统称为“单测”。

这并不是非常严谨,但工程实践中这种术语混用很常见。毕竟软件工程里的很多名词最后都会经历一个阶段:定义写在文档里,实际交流靠猜。


Flutter测试和移动端特性的关系

Flutter 的普通 Unit Test 和 Widget Test 主要运行 Dart / Flutter Framework 部分。

它们不会真正加载:

  • iOS Native代码
  • Android Native代码
  • 系统权限弹窗
  • 系统级组件

所以涉及平台能力时,例如:

  • 相册选择
  • 相机调用
  • 文件权限
  • 推送
  • 生物识别

通常需要 Mock Platform Channel。

例如:

Flutter代码

↓

MethodChannel

↓

iOS / Android 原生实现

普通测试环境:

Flutter代码

↓

Mock MethodChannel

↓

返回模拟结果

所以:

你可以测试:

点击上传按钮

↓

调用照片选择逻辑

↓

拿到图片数据

↓

进入上传流程

但是无法只靠普通单测证明:

iOS照片选择器是否真的能打开

Android权限弹窗是否正常显示

这些需要更高层级的测试,例如 Integration Test。


单测是什么

单测的核心目的:

当未来有人修改这段代码时,能不能快速知道有没有破坏已有能力。

它对应的是更高层级的:

  • 集成测试
  • 端到端测试

单测最大的价值不是证明“现在没有Bug”,而是给未来修改提供安全感。

例如:

今天:

calculatePrice()

运行正常。

半年后:

别人重构代码:

calculatePrice()

如果没有测试:

可能上线后才发现价格计算错误。

如果有测试:

flutter test

↓

失败

↓

发现重构破坏已有逻辑

所以单测可以给系统重构提供信心。


单测的心智模型

你的代码

↓

测试框架启动

↓

调用你的函数 / 组件

↓

获得实际结果

↓

expect比较预期结果

↓

通过 / 失败

例如:

final result = calculatePrice(100);

expect(result, 80);

本质就是:

拿实际运行结果和预期结果比较。


单测和代码设计的关系

单测还有一个额外价值:

它可以帮助发现代码设计问题。

如果一段代码:

  • 依赖很多外部服务
  • 包含很多职责
  • 状态变化复杂

那么测试通常也会变得困难。

例如:

一个函数同时:

调用API

↓

修改数据库

↓

更新缓存

↓

发送通知

↓

修改UI

测试时需要模拟大量环境。

这往往说明代码职责没有拆分。

所以:

单测难写,很多时候意味着代码设计存在耦合问题。


单测是否有必要

对于初期开发:

不一定必须马上写大量单测。

原因:

  • 功能变化快
  • 代码可能快速推翻
  • 人工测试成本较低

这个阶段:

开发速度可能比测试覆盖更重要。

但是对于:

  • 大型项目
  • 长期维护项目
  • 多人协作项目

测试价值会越来越高。

一种常见实践是:

把测试逐渐融入开发流程,例如 TDD。


TDD(Test Driven Development)

经典流程:

先写测试

↓

实现功能

↓

重构代码

也就是:

先定义希望代码满足什么行为,再写实现。

不过实际工程中,不一定所有功能都严格TDD。

很多团队更常见:

开发功能

↓

补充测试

↓

持续维护

尤其探索阶段产品,需求变化非常快,提前给所有代码写完整测试,有时候会出现“代码还没确定,测试已经写了一座城堡”的情况。


Flutter测试分层理解

可以简单理解:

                 Integration Test
                       |
        --------------------------------
        |                              |
   Widget Test                   Native验证
        |
   Unit Test

Unit Test

验证:

  • 数据转换
  • 状态逻辑
  • 算法
  • Service

Widget Test

验证:

  • 用户操作
  • 状态变化
  • UI展示

Integration Test

验证:

  • 真机环境
  • 权限
  • 系统能力
  • 完整业务流程

最终总结:

后端单测主要验证“输入经过逻辑是否得到正确输出”。

前端测试需要额外验证“状态变化是否产生正确UI”。

Flutter因为连接移动端系统能力,所以除了测试Dart逻辑和Widget,还需要通过Integration Test验证真实设备环境。