Flutter快学快用08 单元测试:Flutter 应用单元测试,提升代码质量_dart单元测试_办公模板库 素材蛙的博客-程序员秘密

技术标签: flutter  android  前端  单元测试  教程  

之前已经讲解了 Flutter 所有基础的知识点,本课时介绍如何保证组件代码的质量,以此来确保我们在代码开发过程中或者在重构过程中的代码质量。

单元测试

单元测试的概念是针对程序中最小单位来进行校验的工作,在 Flutter 中最小的单位是组件。由于我们扩展了一些模块比如 Model(Provider)、Struct(数据结构部分),因此这里也需要介绍下这两部分的单元测试。

目录结构

为了保持一致性,我们在 test 单元测试目录,创建与项目结构目录一致的结构,如图 1 所示。

image (9).png
图 1 单元测试目录结构

单元测试目录结构下的测试文件命名也是按照原组件命名方式,但是需要在组件命名后面增加 test 后缀。例如,我们需要对 article_comments.dart 文件进行单元测试,根据规则将其命名为 article_comments_test.dart。

前期准备

首先我们需要在 pubspec.yaml 中增加相应的 flutter_test 第三库,一般项目初始化后,会自动在 dev_dependencies 中引入,最后执行 flutter pub get 更新本地第三库即可。目录结构目前还是需要手动创建,在下一课时,我会在脚手架中自动化创建。

Struct 的单元测试

Struct 的目的是保证数据结构的安全,避免因为动态数据结构而引发客户端的 Crash 问题,因此做好数据结构的单元测试非常必要。Struct 的结构比较简单,只有一个构造函数,在构造函数中存在必须和可选参数,单元测试部分主要是验证这个构造函数即可。

在上一课时中,我们创建了三个 Struct ,这里着重介绍较为复杂的 comment_info_struct.dart 的测试用例写法,代码如下。

import 'package:flutter_test/flutter_test.dart';
import 'package:two_you/util/struct/comment_info_struct.dart';
import 'package:two_you/util/struct/user_info_struct.dart';
void main() {
  final UserInfoStruct userInfo = UserInfoStruct('test', 'http://test.com');
  test('test-userinfo', () {
    final CommentInfoStruct commentInfo =
      CommentInfoStruct(userInfo, 'comment test');
    expect(commentInfo.comment == 'comment test', true);
    expect(commentInfo.userInfo.nickname == 'test', true);
    expect(commentInfo.userInfo.headerImage, 'http://test.com');
  });
}

第 1 行代码引入 flutter_test 第三方库,第 3 和 4 行引入本次测试需要的 struct 结构库。测试文件的所有测试逻辑都在 main 函数中。在第 7 行中使用 UserInfoStruct 创建 userInfo ,Flutter 中的类以及库测试都是以 test 函数为测试方法,test 包含两个参数,一个是测试的描述,另外一个是测试的核心逻辑。

测试的核心逻辑中有一个 expect 方法,该方法可以在代码前使用一个条件判断语句,例如等于、大于、小于等等,而第二个参数可以是任何数据。如果 expect 的前后两个值相等,则测试用例通过,如果不相等则不通过。

代码完成以后,我们在根目录执行下面的命令。

flutter test

执行完成后,就可以看到以下结果,这表明测试用例已全部通过。

00:04 +1: All tests passed!

Model 的单元测试

Model 的测试和 Struct 基本一样,不过在 Model 中有较多方法,因此需要增加一些类方法的测试。这里我们使用 like_num_model.dart 作为测试文件,在 test 目录下的 model 文件夹中新增测试文件 like_num_model_test.dart ,并在实现如下测试代码。

import 'package:flutter_test/flutter_test.dart';
import 'package:two_you/model/like_num_model.dart';
void main() {
  final LikeNumModel likeNumModel = LikeNumModel();
  test('test like model value', () {
    expect(likeNumModel.value, 0);
  });
  test('test like model like method', () {
    likeNumModel.like();
    expect(likeNumModel.value, 1);
    likeNumModel.like();
    expect(likeNumModel.value, 2);
  });
}

代码中第 1 行和第 3 行都是引入相应的库以及测试库文件,其次以 main 为测试入口,在 main 中调用 LikeNumModel 初始化并获得操作句柄,然后分为两部分,一部分测试状态属性,另一部分测试相应状态属性变更的类方法。

组件的单元测试

上面两部分测试代码逻辑较为简单,真正的核心是组件的单元测试。组件测试使用的方法是 testWidgets ,需要将组件放入到 MaterialApp 中,然后在 MaterialApp 中去 find 相应组件中的元素,接下来我们看一个比较简单的无状态组件的测试 。

无状态组件

学习无状态组件的单元测试,我们选择上一课时中 article_detail 文件下的 article_content.dart 组件作为例子。在 test/article_detail 文件夹中创建 article_content_test.dart 文件,代码实现如下。

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:two_you/widgets/article_detail/article_content.dart';
void main() {
  testWidgets('test article content', (WidgetTester tester) async {
    final Widget testWidgets = ArticleContent(content: 'test content');
    await tester.pumpWidget(
        new MaterialApp(
            home: testWidgets
        )
    );
    expect(find.text('test content'), findsOneWidget);
    expect(find.byWidget(testWidgets), findsOneWidget);
  });
}
  • 代码的前 2 行引入相应的组件库和测试库,第 4 行引入需要被测试的组件 article_content ;

  • 在 main 函数中使用 testWidgets 来测试组件,testWidgets 也有两个参数,第一个是测试描述,第二个是一个执行函数,函数会自带一个组件测试对象 tester ;

  • 在测试过程中需要将被测试的组件插入到 MaterialApp ,因此这里需要使用到tester.pumpWidget 方法,代码在第 9 行中体现;因为这是一个异步方法,因此需要函数使用 async ,并且这里需要使用 await 来等待执行完成;

  • 使用 expect 来查询组件,findsOneWidget 来判断是否找到相应的组件。

以上就是无状态组件的测试方法,由于上面的 article_content 内部只有一个 text 组件,因此单元测试比较简单。无状态组件可以验证组件是否存在,并且可以判断组件中的元素是否按照参数传入的值显示。

有状态组件

有状态组件在组件测试部分与无状态组件一样,这里主要是介绍在组件触发更新后,如何保证界面显示正常与否。这里我们使用上一课时的 article_detail_like 作为测试例子。因为组件状态管理需要使用 Provider ,因此需要引入该模块。

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:two_you/model/like_num_model.dart';
import 'package:two_you/widgets/article_detail/article_detail_like.dart';

接下来在 main 函数初始化状态模块 like_num_model,代码如下。

void main() {
  final LikeNumModel likeNumModel = LikeNumModel();
}

然后我们增加单纯的静态组件测试,这部分和无状态组件部分完全一致,代码如下。

testWidgets('test article like widget', (WidgetTester tester) async {
  final Widget testWidgets = ArticleDetailLike();
  await tester.pumpWidget(
      new Provider<int>.value(
          child: ChangeNotifierProvider.value(
            value: likeNumModel,
            child: MaterialApp(
                home: testWidgets
            ),
          )
      )
  );
  expect(find.byType(FlatButton), findsOneWidget);
  expect(find.byIcon(Icons.thumb_up), findsOneWidget);
  expect(find.text('0'), findsOneWidget);
});

与无状态组件测试唯一不同的是,我们需要使用 Provider 将 MaterialApp 封装起来。在代码中的第 13 行找 FlatButton 组件,第 14 行寻找 thumb_up icon ,第 15 行获取组件中的 Text 组件,并判断初始值为 0 。

接下来我们看下比较复杂的事件触发更新的测试部分逻辑。在这个例子的单元测试中,我们需要触发按钮点击操作,并且进行 rebuild 后,重新校验组件的正确性,代码如下。

testWidgets('test article like widget when like action', (WidgetTester tester) async {
  final Widget testWidgets = ArticleDetailLike();
  await tester.pumpWidget(
      new Provider<int>.value(
          child: ChangeNotifierProvider.value(
            value: likeNumModel,
            child: MaterialApp(
                home: testWidgets
            ),
          )
      )
  );
  await tester.tap(find.byType(FlatButton));
  await Future.microtask(tester.pump);
  expect(find.text('1'), findsOneWidget);
});

代码中的第 13 行就是找到 FlatButton 并且触发其点击操作,使用的是 tester.tap 方法,在触发后需要等待组件重新更新,因此需要使用 Future.microtask 来触发等待更新完成,完成后再校验组件中的点赞数是否更新,在上面的 17 行中使用 expect 再次判断。

综合实践

以上就囊括了所有的单元测试的写法,由于断言 find 的方法还存在其他比较多的用法,这里就不复制过来,具体详细的内容,大家可以前往官方文档去查询。

接下来大家需要将上一课时的所有的组件使用本课时的知识点,覆盖到所有的单元测试,写完以后大家可以对比或者参考我们 github 上的源码。

这里也补充下,因为涉及图片组件,为了避免图片组件在测试加载过程中的异常问题,这里需要使用第三方库 image_test_utils ,下面是一个使用该组件的例子。

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

import ‘package:two_you/util/struct/article_summary_struct.dart’;
import ‘package:two_you/widgets/home_page/article_summary.dart’;

void main() {
  /// 帖子概要描述信息
  final ArticleSummaryStruct articleInfo = ArticleSummaryStruct(
      ‘你好,交个朋友’,
      ‘我是一个小可爱,很长的一个测试看看效果,会换行吗’,
      ‘https://i.pinimg.com/originals/e0/64/4b/e0644bd2f13db50d0ef6a4df5a756fd9.png’,
      20,
      30);

  testWidgets(‘test article summary’, (WidgetTester tester) async {
    provideMockedNetworkImages(() async {
      final Widget testWidgets = ArticleSummary(
          title: articleInfo.title,
          summary: articleInfo.summary,
          articleImage: articleInfo.articleImage
      );
      await tester.pumpWidget(
          new MaterialApp(
              home: testWidgets
          )
      );

      expect(find.text(‘你好,交个朋友’), findsOneWidget);
      expect(find.text(‘我是一个小可爱,很长的一个测试看看效果,会换行吗’), findsOneWidget);

      expect(find.byWidget(testWidgets), findsOneWidget);
    });
  });
}

主要看代码的第 22 行,需要将整个测试代码使用 provideMockedNetworkImages 函数来执行,这样就不会出现异常情况了。

总结

以上就是本课时的所有内容,学完本课时你需要掌握 Struct、Model、无状态和有状态组件的单元测试写法。

下一课时我将把我们基础部分的所有基础知识汇总会一个脚手架,规范和统一基础模块。谢谢。

点击此链接查看本课时源码


精选评论

**河:

老师,请问一下单元测试的作用是什么,平时开发接上数据直接调不是更快吗

    讲师回复:

    如果你形容当前快的话,那是肯定的。但是你放长远来看,如果你这个组件或者代码需要维护,比如新需求,涉及到这部分那就涉及到重构或者新增功能。如果这时候你没有单元测试,那是不是又需要把原来自测的逻辑走一遍,如果你有单元测试,那是不是可以直接跑一遍原来旧的测试用例,这样重构或者改造引来的问题都会很少。

**佳:

请问老师我运行flutter test之后会报错,内容如下:Failed to load “D:\flutterDaemon\overAll\test\util\struct\comment_info_struct_test.dart”:Shell subprocess crashed with unexpected exit code -1073740791 before connecting to test harness.Test: D:\flutterDaemon\overAll\test\util\struct\comment_info_struct_test.dartShell: D:\flutter_windows_1.17.0-stable\flutter\bin\cache\artifacts\engine\windows-x64\flutter_tester.exe

    讲师回复:

    你运行下08课时里面的代码,记得运行的时候要在项目根目录。其次你换个最新的flutter版本,看官网在某个版本会出现运行 flutter test 在windows下会crash的情况,说已经修复。目前最新的是1.22,你试试。

**伟:

老师,这个Future.microtask的参数值跟tester.pump的返回值类型不一致怎么解决

    讲师回复:

    我没看到你具体的代码,如果不一致,检查下是否有多个地方运行这条测试用例,两边数据都进行了修改,导致了两个数据不一样。你可以在代码逻辑中增加调试信息,看下是哪部分对数据进行了修改导致两者不一致。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/fegus/article/details/126948531

智能推荐

python之flask框架(一)_找BUG的老爷的博客-程序员秘密

python基础部分咱们基本已经掌握,前面也了解了tkinter的基本用法,这次给大家讲解一下很常见的web框架,就是这个flask框架。其实,python的web框架不仅仅是flask,还有Django,Weppy,Bottle等还有很多。其中flask框架呢是一个使用python编写的轻量级 Web 应用框架。其 WSGI 工具箱采用 Werkzeug ,模板引擎则使用 Jinja2 。Flask使用 BSD 授权。而且这个框架相对来说也是比较简单,非常适合初学者学习那么简单的fla...

Echarts之饼图制作_CHAO_YE的博客-程序员秘密_echarts饼图

这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是必不可少的KaTeX数学公式新的甘特图功能,丰富你的文章UML 图表FLowchart流程图导出与导入导出导入本篇主要讲解通过echarts制作一个饼图时所用到的一些代码,及所用到代码的详细作用,记录自己的echarts学习过程,同时

大整数加法_尼尔先生的博客-程序员秘密

#include&amp;lt;stdio.h&amp;gt;#include&amp;lt;string.h&amp;gt;#include&amp;lt;math.h&amp;gt;int main(){ char s[400]; char t[400]; int m,n,max,d; int a[400],b[400],c[400]; scanf(&quot;%s&quot;, s); scanf(&quot;%s&quot;, t); m = strle...

css当元素没有宽高时水平垂直居中,行内元素居中,不同寻常的居中办法_Chris__Wang的博客-程序员秘密

对于css中元素水平垂直居中的方法也有很多1. 有对固定宽高元素进行水平垂直居中的2.也有对不固定宽高的元素进行水平垂直居中代码如下 .son { position: absolute; background: green; margin:auto; left: 0; ...

Soul网关源码学习(11)- Http代理的负载均衡:DividePlugin 和 SpringCloudPlugin_木瓜饭的博客-程序员秘密

文章目录前言DevidePlugin负载均衡策略 - 加权随机负载均衡策略 - 一致性 Hash负载均衡策略 - 加权轮询SpringCloudPlugin总结参考资料前言在上一篇文章《Soul网关源码学习(10)- 插件模板 AbstractSoulPlugin》 中,我们分析了插件的模板抽象类 AbstractSoulPlugin,AbstractSoulPlugin #executor 方法主要抽象了 PluginData、SelectorData、Rule 的获取和筛选流程,并将它们作为参数传递

Windows如何安装WSL(中途退出后遇到bug该怎么办)_zhonguncle的博客-程序员秘密_wsl --install不出现安装

适用于 Linux 的 Windows 子系统,被简称为WSL。安装方式很简单。如果你是第一次安装,打开Windows PowerShell(推荐使用Windows Termianl,可以使用商店下载安装),输入以下命令会自动安装Ubuntu:wsl --install但是如果你觉得太慢关掉了,或者出现问题退出了,或者想安装其他的Linux发行版的话,那么就需要使用以下方式。首先使用以下命令查看可安装的Linux发行版列表:wsl --list --online结果如下:以下是可安装的有效

随便推点

Mybatis源码分析----Mybatis数据源与连接池(1)_天高任鸟飞-海阔凭鱼跃的博客-程序员秘密_mybatis 数据源 无限

对于ORM框架而言,数据源的组织是一个非常重要的一部分,这直接影响到框架的性能问题。本文将通过对MyBatis框架的数据源结构进行详尽的分析,并且深入解析MyBatis的连接池。    本文首先会讲述MyBatis的数据源的分类,然后会介绍数据源是如何加载和使用的。紧接着将分类介绍UNPOOLED、POOLED和JNDI类型的数据源组织;期间我们会重点讲解POOLED类型的数据源和其实现的连接...

python学习-day12 time和datetime模块_laywl的博客-程序员秘密

视频链接:https://www.bilibili.com/video/BV1SE411N7Hi?p=77详细内容:

SQL难点解决:集合及行号_邹小青的博客-程序员秘密

相关文章:《SQL难点解决:记录的引用》《SQL 难点解决:直观分组》《SQL 难点解决:序列生成》《MySQL难点解决:实现Oracle高级分析函数》《MySQL难点解决:窗口函数》 1. 和集示例1:求重叠时间段的总天数MySQL8:with recursive t(start,end) as (select date'2010-01-07',date'...

使用Typora时LaTeX内联公式简单汇总_白白旧维的博客-程序员秘密_typora内联公式

0、前言在使用Typora写笔记时,有时候需要添加上LaTeX排版的公式。以下是整理出的一份简单笔记,方便记忆不清楚时查阅。笔记暂时不够完善,之后再补充。注:有些公式语法KateX不能解析,但在Typora中可以使用(不用在意)1、一些基本内联公式位置关系展示LaTeX说明x2x^{2}x2x^{2}右上标x2x_{2}x2​x_{2}右下标2x{}^{2}x2x{}^{2}x左上标2x{}_{2}x2​x{}_{2}x左下标

码率直降70%,拍乐云发布国内首个 AV1 编码引擎 Venus,引领实时视频互动革新..._音视频开发进阶的博客-程序员秘密

近日,拍乐云音视频实验室发布了国内首个应用在实时视频服务的低码高清编码引擎 Pano Venus。这是新一代视频编码标准 AV1 第一次在国内实时系统中的应用落地,用以取代现有的主流编码标...

sso cas连续登录两次刷新页面问题解决_探索丶挑战丶突破的博客-程序员秘密_cas 跳转一直刷新令牌

问题情况:1.、使用cas 单点登录时,输入账号密码,第一次输出错误,提示密码输入错误,当第二次输入正确时页面刷新,文本框的账号密码都是空的,详细情况请看GIF 解决方法及思路:1、因为在没有改造cas登录页面前没有发生此类问题,必然是改造后出现的问题。2、进入cas登录页面F12查看所有请求,发现几处302请求3、发现是没有引用到js脚本,再到项目中看,原来在top.js...

推荐文章

热门文章

相关标签