我的 Flutter TDD 心路历程

本文原始发表于:https://juejin.cn/post/7038890714507247624

导语: Test-driven development (TDD) 在当前国内很多软件开发人员理解中比较模糊,大部分人也没有明确和有意识的去实施 TDD,因此很多人都有着不同的理解,包括我本人在实践 TDD 之前都比较排斥。不过有句话说得好:“实践是检验真理的唯一标准,任何没有经过实践就轻易下的结论都是耍流氓”(后半句话是我说的,没错) 本文记录了我在 Flutter 中实践 TDD 的一些所思所考,全文根据真实经历,没有改编,仅供参考

阅读前提:对 FlutterDartFlutter 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(),
    );
  }
}

这样,之前的用例就能跑过了

image-20211201170151990

思考:可以看到当前的实现很挫,是不符合我们功能的预期的,而是仅仅能够让用例通过的实现版本。按照我们常规的开发流程或者习惯,我们在实现的时候可能会忍不住想去优化代码,去想各种边界条件,然后写出一个比较完善的实现版本。例如这里我们可能习惯性定义好各种状态的枚举,然后在 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: "没有找到空列表控件");
    });

用例运行失败

增加这个用例之后,现在跑一下单测:第一个用例成功,第二个用例失败

image-20211203111955018

显而易见,之前我们只实现了 loading 状态,甚至都没有判断

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是一个简单的Flutter登录页面的代码示例: ```dart import 'package:flutter/material.dart'; class LoginPage extends StatefulWidget { @override _LoginPageState createState() => _LoginPageState(); } class _LoginPageState extends State<LoginPage> { final _formKey = GlobalKey<FormState>(); String _email; String _password; void _submitForm() { if (_formKey.currentState.validate()) { _formKey.currentState.save(); // TODO: Perform login operation print("Email: $_email"); print("Password: $_password"); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Login"), ), body: Padding( padding: EdgeInsets.all(20.0), child: Form( key: _formKey, child: Column( children: <Widget>[ TextFormField( decoration: InputDecoration(labelText: "Email"), validator: (value) { if (value.isEmpty) { return "Please enter your email"; } return null; }, onSaved: (value) { _email = value; }, ), TextFormField( decoration: InputDecoration(labelText: "Password"), obscureText: true, validator: (value) { if (value.isEmpty) { return "Please enter your password"; } return null; }, onSaved: (value) { _password = value; }, ), SizedBox(height: 20.0), RaisedButton( onPressed: _submitForm, child: Text("Login"), ) ], ), ), ), ); } } ``` 在这个示例中,我们使用了一个带有表单验证和保存的Flutter StatefulWidget来创建登录页面。在表单提交时,我们可以执行实际登录操作。你可以根据自己的需要修改代码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值