本文原始发表于:https://juejin.cn/post/7038890714507247624
导语: Test-driven development (TDD) 在当前国内很多软件开发人员理解中比较模糊,大部分人也没有明确和有意识的去实施 TDD,因此很多人都有着不同的理解,包括我本人在实践 TDD 之前都比较排斥。不过有句话说得好:“实践是检验真理的唯一标准,任何没有经过实践就轻易下的结论都是耍流氓”(后半句话是我说的,没错) 本文记录了我在 Flutter 中实践 TDD 的一些所思所考,全文根据真实经历,没有改编,仅供参考
阅读前提:对 Flutter
、Dart
、Flutter test
以及 TDD
稍有了解
0. 怀疑和抗拒
- 感受不到
TDD
带来的价值,TDD
打破了常规的开发思路 - 觉得
TDD
繁琐,明明可以一口气实现的代码,为什么非要拆细 - 先写用例,但是无从下手,怎么设计用例
- 觉得写的用例有点傻,感觉没什么用
- 我写的代码逻辑很简单,肯定不会有问题,没必要写单测
- 写着写着发现之前的用例好像不太对,想改用例?
- 用例怎么拆?怎么控制粒度?
- 什么时候才重构?
1. 从无到有
案例:实现一个通用的支持上滑加载下拉刷新的 Flutter
列表
用例梳理:
- 加载过程显示 loading 动画
- 加载结果为空列表显示 empty 页面
- 加载结果失败显示 error 页面
- …
一开始只梳理出三个用例,为了聚焦,没有考虑所有场景,理论上 TDD
是可以慢慢补充用例完善功能的,先聚焦这三个相对简单的用例
尝试一下 TDD
流程:先写单测用例 -> 用例失败 -> 编写最小可运行单测版本的实现
1.1 第一个用例:加载过程显示 loading 动画
先写单测
思考:当前没有任何实现代码,意味着单测怎么写完全跳脱出具体实现,那肯定是怎么简单怎么来(不需要 mock),这里甚至不考虑合理性,先把用例需求用单测代码描述出来
**Given:**首先我肯定需要准备一个 Widget
,因为三个用例是不同加载状态对应不同显示 Widget
,那我暂且设计成这个 Widget
需要一个 Status
入参,先不考虑合理性和扩展性,至少目前是可测的(后面会涉及重构)
When: 加载 Widget
,并传入参数为 loading
表示加载中
**Then:**验证当前页面是否有 loading widget
出现
编码实现:
void main() {
testWidgets("列表加载状态显示 loading", (tester) async {
FeedList feedList = const FeedList(loadingStatus: LoadingStatus.loading);
await tester.pumpWidget(MaterialApp(home: feedList));
var loadingFinder = find.bySemanticsLabel("feed_loading");
expect(loadingFinder, findsOneWidget, reason: "没有找到 loading 控件");
});
}
用例运行失败
这个用例目前肯定是跑不过的
第一,根本没有 FeedList
这个 widget
第二,也不可能有 feed_loading
这个 semantics 的 widget
编写最小可运行单测版本的实现
enum LoadingStatus {
loading,
}
class FeedList extends StatelessWidget {
final LoadingStatus loadingStatus;
const FeedList({
Key? key,
required this.loadingStatus,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// 注意:这里压根就没有判断状态,而是直接就显示一个 loading 态
// 因为目前只有一个用例,这样的代码就已经能让用例通过了
return Semantics(
label: "feed_loading",
child: Container(),
);
}
}
这样,之前的用例就能跑过了
思考:可以看到当前的实现很挫,是不符合我们功能的预期的,而是仅仅能够让用例通过的实现版本。按照我们常规的开发流程或者习惯,我们在实现的时候可能会忍不住想去优化代码,去想各种边界条件,然后写出一个比较完善的实现版本。例如这里我们可能习惯性定义好各种状态的枚举,然后在 build
的时候判断各种状态,实现各个状态的处理逻辑。这个看来很顺手的事情,我们现在暂且不做,按照 TDD
的开发流程,到这一步我们是坚决不能过早地去优化代码,去编写用例以外的实现的。先记住一个原则:我们所写的每一行代码,都尽可能先编写好测试用例来覆盖,即先写测试用例,再写实现
这里我们先忍着不着急去优化或者重构,我们继续往下
1.2 第二个用例:加载结果为空列表显示 empty 页面
先写单测
有了之前的代码,第二个用例自然而然就是换个状态入参即可,这也说明我们之前的设计到目前为止还是比较可测的,代码如下
testWidgets("加载结束之后空列表状态显示空列表 widget", (tester) async {
FeedList feedList = const FeedList(loadingStatus: LoadingStatus.empty);
await tester.pumpWidget(MaterialApp(home: feedList));
var loadingFinder = find.bySemanticsLabel(FeedList.semanticsFeedEmpty);
expect(loadingFinder, findsOneWidget, reason: "没有找到空列表控件");
});
用例运行失败
增加这个用例之后,现在跑一下单测:第一个用例成功,第二个用例失败
显而易见,之前我们只实现了 loading 状态,甚至都没有判断