Flutter 中的单元测试:从工作流基础到复杂场景

对 Flutter 的兴趣空前高涨——而且早就应该出现了。 Google 的开源 SDK 与 Android、iOS、macOS、Web、Windows 和 Linux 兼容。单个 Flutter 代码库支持所有这些。单元测试有助于交付一致且可靠的 Flutter 应用程序,通过在组装之前先发制人地提高代码质量来确保不会出现错误、缺陷和缺陷。

在本教程中,分享了 Flutter 单元测试的工作流程优化,演示了基本的 Flutter 单元测试,然后转向更复杂的 Flutter 测试用例和库。

Flutter单元测试的流程

在 Flutter 实现单元测试的方式与在其他技术栈中的方式大致相同:

1.评估代码

2.设置模拟数据

3.定义测试组

4.为每个测试组定义测试函数签名

5.写测试用例

为了演示单元测试,我准备了一个示例 Flutter 项目。该项目使用外部 API 来获取和显示可以按国家过滤的大学列表。

关于 Flutter 工作原理的一些注意事项: 该框架通过在创建项目时自动加载 flutter_test库来促进测试。该库使 Flutter 能够读取、运行和分析单元测试。Flutter 还会自动创建用于存储测试的test文件夹。避免重命名和/或移动test文件夹至关重要,因为这会破坏其功能,从而破坏运行测试的能力。在测试文件名中包含 _test.dart也很重要,因为这个后缀是 Flutter 识别测试文件的方式。

测试目录结构

为了在项目中进行单元测试,使用干净的架构实现了 MVVM和依赖注入 (DI) ,正如为源代码子文件夹选择的名称所证明的那样。MVVM 和 DI 原则的结合确保了关注点分离:

1.每个项目类都支持一个目标。

2.类中的每个函数只完成它自己的范围。

给编写的测试文件创建一个有组织的存储空间,在这个系统中,测试组将具有易于识别的“家”。鉴于 Flutter 要求在测试文件夹中定位测试,我们将test目录下test文件组织成和源码相同的结构。然后,编写测试时,将其存储在适当的子文件夹中:就像干净的袜子放在梳妆台的袜子抽屉里,折叠的衬衫放在衬衫抽屉里一样,Model类的单元测试放在名为 model 的文件夹中 , 例如。

图片

项目的测试文件夹结构反映了源代码结构,采用此文件系统可以使项目透明化,并为团队提供一种简单的方法来查看代码的哪些部分具有相关测试。现在准备将单元测试付诸实践。

一个简单的 Flutter 单元测试

现在将从model类(在源代码的data层中)开始,并将示例限制为仅包含一个model类 ApiUniversityModel。此类拥有两个功能:

●通过使用 Map模拟 JSON 对象来初始化模型。
●构建University数据模型。

为了测试模型的每个功能,这里自定义一下前面描述的通用步骤:

1.评估代码

2.设置数据模拟:将定义服务器对 API 调用的响应

3.定义测试组:将有两个测试组,每个功能一个

4.为每个测试组定义测试函数签名

5.编写测试用例

评估我们的代码后,我们准备实现第二个目标:设置特定于ApiUniversityModel类中的两个函数的数据模拟。
为了模拟第一个函数(通过使用 Map模拟 JSON 来初始化模型)fromJson,创建两个 Map 对象来模拟函数的输入数据。再创建两个等效的 ApiUniversityModel 对象,以表示具有所提供输入的函数的预期结果。
为了模拟第二个函数(构建University数据模型)toDomain,创建两个University对象,这是在先前实例化的ApiUniversityModel 对象中运行此函数后的预期结果:

void main() {Map<String, dynamic> apiUniversityOneAsJson = {"alpha_two_code": "US","domains": ["marywood.edu"],"country": "United States","state-province": null,"web_pages": ["http://www.marywood.edu"],"name": "Marywood University"};ApiUniversityModel expectedApiUniversityOne = ApiUniversityModel(alphaCode: "US",country: "United States",state: null,name: "Marywood University",websites: ["http://www.marywood.edu"],domains: ["marywood.edu"],);University expectedUniversityOne = University(alphaCode: "US",country: "United States",state: "",name: "Marywood University",websites: ["http://www.marywood.edu"],domains: ["marywood.edu"],);Map<String, dynamic> apiUniversityTwoAsJson = {"alpha_two_code": "US","domains": ["lindenwood.edu"],"country": "United States","state-province":"MJ","web_pages": null,"name": "Lindenwood University"};ApiUniversityModel expectedApiUniversityTwo = ApiUniversityModel(alphaCode: "US",country: "United States",state:"MJ",name: "Lindenwood University",websites: null,domains: ["lindenwood.edu"],);University expectedUniversityTwo = University(alphaCode: "US",country: "United States",state: "MJ",name: "Lindenwood University",websites: [],domains: ["lindenwood.edu"],);}

接下来,第三个和第四个目标,将添加描述性语言来定义测试组和测试函数签名:

   void main() {// Previous declarationsgroup("Test ApiUniversityModel initialization from JSON", () {test('Test using json one', () {});test('Test using json two', () {});});group("Test ApiUniversityModel toDomain", () {test('Test toDomain using json one', () {});test('Test toDomain using json two', () {});});}

现在定义了两个测试的签名来检查 fromJson 函数,两个测试来检查 toDomain函数。
为了实现第五个目标并编写测试,将使用 flutter_test库的 expect 方法将函数的结果与预期进行比较:

void main() {// Previous declarationsgroup("Test ApiUniversityModel initialization from json", () {test('Test using json one', () {expect(ApiUniversityModel.fromJson(apiUniversityOneAsJson),expectedApiUniversityOne);});test('Test using json two', () {expect(ApiUniversityModel.fromJson(apiUniversityTwoAsJson),expectedApiUniversityTwo);});});group("Test ApiUniversityModel toDomain", () {test('Test toDomain using json one', () {expect(ApiUniversityModel.fromJson(apiUniversityOneAsJson).toDomain(),expectedUniversityOne);});test('Test toDomain using json two', () {expect(ApiUniversityModel.fromJson(apiUniversityTwoAsJson).toDomain(),expectedUniversityTwo);});});}

完成五个目标后,现在可以从 IDE 或命令行运行测试。

图片

在终端,可以通过输入 flutter test 命令来运行test文件夹中包含的所有测试,并查看测试是否通过。或者,可以通过输入 flutter test --plain-name "ReplaceWithName"命令来运行单个测试或测试组,用测试或测试组的名称替换 ReplaceWithName。

在 Flutter 中对端点进行单元测试

完成了一个没有依赖项的简单测试后,让我们探索一个更有趣的示例:将测试endpoint类,其范围包括:

●执行对服务器的 API 调用。
●将 API JSON 响应转换为不同的格式。
在评估了代码之后,将使用 flutter_test库的 setUp方法来初始化测试组中的类:

group("Test University Endpoint API calls", () {setUp(() {baseUrl = "https://test.url";dioClient = Dio(BaseOptions());endpoint = UniversityEndpoint(dioClient, baseUrl: baseUrl);});}

要向 API 发出网络请求,更喜欢使用改造库,它会生成大部分必要的代码。 为了正确测试 UniversityEndpoint类,将强制 dio 库(Retrofit 用于执行 API 调用)通过自定义响应适配器模拟 Dio 类的行为来返回所需的结果。

自定义网络拦截器

由于通过 DI 构建了UniversityEndpoint类,因此可以进行自定义网络拦截器。 (如果 UniversityEndpoint 类自己初始化一个 Dio 类,就没有办法模拟类的行为。)
为了模拟Dio类的行为,需要知道 Retrofit库中使用的 Dio方法—— 但无法直接访问 Dio。 因此,将使用自定义网络响应拦截器模拟 Dio:

class DioMockResponsesAdapter extends HttpClientAdapter {final MockAdapterInterceptor interceptor;DioMockResponsesAdapter(this.interceptor);@overridevoid close({bool force = false}) {}@overrideFuture<ResponseBody> fetch(RequestOptions options,Stream<Uint8List>? requestStream, Future? cancelFuture) {if (options.method == interceptor.type.name.toUpperCase() &&options.baseUrl == interceptor.uri &&options.queryParameters.hasSameElementsAs(interceptor.query) &&options.path == interceptor.path) {return Future.value(ResponseBody.fromString(jsonEncode(interceptor.serializableResponse),interceptor.responseCode,headers: {"content-type": ["application/json"]},));}return Future.value(ResponseBody.fromString(jsonEncode({"error": "Request doesn't match the mock interceptor details!"}),-1,statusMessage: "Request doesn't match the mock interceptor details!"));}}enum RequestType { GET, POST, PUT, PATCH, DELETE }class MockAdapterInterceptor {final RequestType type;final String uri;final String path;final Map<String, dynamic> query;final Object serializableResponse;final int responseCode;MockAdapterInterceptor(this.type, this.uri, this.path, this.query,this.serializableResponse, this.responseCode);}

现在已经创建了拦截器来模拟网络响应,接下来可以定义测试组和测试函数签名。在例子中,只有一个函数要测试 (getUniversitiesByCountry),因此将只创建一个测试组。现测试函数对三种情况的响应:

1.Dio类的函数是否真的被 getUniversitiesByCountry 调用了?

2.如果API 请求返回错误,会发生什么?

3.如果 API 请求返回预期结果,会发生什么?

这是测试组和测试函数签名:

 group("Test University Endpoint API calls", () {test('Test endpoint calls dio', () async {});test('Test endpoint returns error', () async {});test('Test endpoint calls and returns 2 valid universities', () async {});});

现在准备好编写测试用例了。对于每个测试用例,要创建一个具有相应配置的 DioMockResponsesAdapter 实例:

group("Test University Endpoint API calls", () {setUp(() {baseUrl = "https://test.url";dioClient = Dio(BaseOptions());endpoint = UniversityEndpoint(dioClient, baseUrl: baseUrl);});test('Test endpoint calls dio', () async {dioClient.httpClientAdapter = _createMockAdapterForSearchRequest(200,[],);var result = await endpoint.getUniversitiesByCountry("us");expect(result, <ApiUniversityModel>[]);});test('Test endpoint returns error', () async {dioClient.httpClientAdapter = _createMockAdapterForSearchRequest(404,{"error": "Not found!"},);List<ApiUniversityModel>? response;DioError? error;try {response = await endpoint.getUniversitiesByCountry("us");} on DioError catch (dioError, _) {error = dioError;}expect(response, null);expect(error?.error, "Http status error [404]");});test('Test endpoint calls and returns 2 valid universities', () async {dioClient.httpClientAdapter = _createMockAdapterForSearchRequest(200,generateTwoValidUniversities(),);var result = await endpoint.getUniversitiesByCountry("us");expect(result, expectedTwoValidUniversities());});});

现在端点测试已经完成,开始测试数据源类 UniversityRemoteDataSource。早些时候,可以看到UniversityEndpoint类是构造函数UniversityRemoteDataSource({UniversityEndpoint? universityEndpoint}) 的一部分,这表明 UniversityRemoteDataSource使用 UniversityEndpoint 类来实现其范围,因此这是将模拟的类。

使用 Mockito 进行模拟

在之前的示例中,使用自定义 NetworkInterceptor 手动模拟了 Dio 客户端的请求适配器。手动执行此操作(模拟类及其函数)将非常耗时。 幸运的是,模拟库旨在处理此类情况,并且可以毫不费力地生成模拟类。 使用 mockito 库,这是 Flutter 中用于模拟的行业标准库。为了通过 Mockito 进行模拟,
首先在测试代码之前添加注释“@GenerateMocks([class_1,class_2,…])”——就在void main() {}函数之上。 在注释中,将包含一个类名列表作为参数(代替 class_1、class_2…)。
接下来,运行 Flutter 的flutter pub run build_runner构建命令,在与测试相同的目录中为我们的模拟类生成代码。 生成的模拟文件的名称将是测试文件名加上.mocks.dart的组合,替换测试的 .dart后缀。
该文件的内容将包括名称以前缀 Mock开头的模拟类。 例如,UniversityEndpoint 变为 MockUniversityEndpoint。
现在,将 university_remote_data_source_test.dart.mocks.dart(模拟文件)导入 university_remote_data_source_test.dart(测试文件)。
然后,在 setUp 函数中,通过使用 MockUniversityEndpoint并初始化 UniversityRemoteDataSource类来模拟 UniversityEndpoint:

import 'university_remote_data_source_test.mocks.dart';@GenerateMocks([UniversityEndpoint])void main() {late UniversityEndpoint endpoint;late UniversityRemoteDataSource dataSource;group("Test function calls", () {setUp(() {endpoint = MockUniversityEndpoint();dataSource = UniversityRemoteDataSource(universityEndpoint: endpoint);});}

成功模拟了UniversityEndpoint,然后初始化了UniversityRemoteDataSource 类。 现在准备好定义测试组和测试函数签名:

group("Test function calls", () {test('Test dataSource calls getUniversitiesByCountry from endpoint', () {});test('Test dataSource maps getUniversitiesByCountry response to Stream', () {});test('Test dataSource maps getUniversitiesByCountry response to Stream with error', () {});});

这样,模拟、测试组和测试函数签名就设置好了。 已准备好编写实际测试。
第一个测试检查当数据源启动国家信息获取时是否调用了 UniversityEndpoint 函数。 首先定义每个类在调用其函数时将如何反应。 由于模拟了 UniversityEndpoint类,这就是将使用的类,使用 when(function_that_will_be_called).then(what_will_be_returned)代码结构。
正在测试的函数是异步的(返回 Future 对象的函数),因此使用when(function name).thenanswer( () {modified function result} )代码结构来修改结果。要检查 getUniversitiesByCountry 函数是否调用了 UniversityEndpoint类中的 getUniversitiesByCountry 函数,使用 when(…).thenAnswer( () {…} )来模拟 UniversityEndpoint 类中的 getUniversitiesByCountry 函数:

when(endpoint.getUniversitiesByCountry("test")).thenAnswer((realInvocation) => Future.value(<ApiUniversityModel>[]));

现在已经模拟了响应,调用数据源函数并使用验证函数检查是否调用了UniversityEndpoint函数:

test('Test dataSource calls getUniversitiesByCountry from endpoint', () {when(endpoint.getUniversitiesByCountry("test")).thenAnswer((realInvocation) => Future.value(<ApiUniversityModel>[]));dataSource.getUniversitiesByCountry("test");verify(endpoint.getUniversitiesByCountry("test"));});

可以使用相同的原则来编写额外的测试来检查函数是否正确地将端点结果转换为相关的数据流:

import 'university_remote_data_source_test.mocks.dart';@GenerateMocks([UniversityEndpoint])void main() {late UniversityEndpoint endpoint;late UniversityRemoteDataSource dataSource;group("Test function calls", () {setUp(() {endpoint = MockUniversityEndpoint();dataSource = UniversityRemoteDataSource(universityEndpoint: endpoint);});test('Test dataSource calls getUniversitiesByCountry from endpoint', () {when(endpoint.getUniversitiesByCountry("test")).thenAnswer((realInvocation) => Future.value(<ApiUniversityModel>[]));dataSource.getUniversitiesByCountry("test");verify(endpoint.getUniversitiesByCountry("test"));});test('Test dataSource maps getUniversitiesByCountry response to Stream',() {when(endpoint.getUniversitiesByCountry("test")).thenAnswer((realInvocation) => Future.value(<ApiUniversityModel>[]));expect(dataSource.getUniversitiesByCountry("test"),emitsInOrder([const AppResult<List<University>>.loading(),const AppResult<List<University>>.data([])]),);});test('Test dataSource maps getUniversitiesByCountry response to Stream with error',() {ApiError mockApiError = ApiError(statusCode: 400,message: "error",errors: null,);when(endpoint.getUniversitiesByCountry("test")).thenAnswer((realInvocation) => Future.error(mockApiError));expect(dataSource.getUniversitiesByCountry("test"),emitsInOrder([const AppResult<List<University>>.loading(),AppResult<List<University>>.apiError(mockApiError)]),);});});}

我们已经执行了许多 Flutter 单元测试并演示了不同的模拟方法。 可以继续使用示例Flutter 项目来运行其他测试。

Flutter 单元测试:实现卓越用户体验的关键

如果已经将单元测试整合到 Flutter 项目中,本文可能已经介绍了一些可以注入到工作流程中的新选项。 在本教程中,演示了将单元测试合并到下一个 Flutter 项目中是多么简单,以及如何应对更细微的测试场景的挑战。你可能再也不想跳过 Flutter 中的单元测试了。

最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

在这里插入图片描述

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!   

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/131872.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构与算法(二)——前缀、中缀、后缀表达式

一、前缀表达式&#xff08;波兰表达式&#xff09; 1.1、计算机求值 从右至左扫描表达式&#xff0c;遇到数字时&#xff0c;将数字压入堆栈。遇到运算符时&#xff0c;弹出栈顶的两个数&#xff0c;用运算符对它们做相应的计算&#xff08;栈顶元素 和 次顶元素&#xff09…

Navicat导入Excel数据顺序变了

项目场景&#xff1a; Navicat导入Excel数据 问题描述 从Excel表格中导入数据到数据库中。但是&#xff0c;在导入的过程中&#xff0c;我们常会发现数据顺序出现了问题&#xff0c;导致数据错位&#xff0c;给数据的处理带来了极大的麻烦。 原因分析&#xff1a; 这个问题的…

mybatisplus配置拦截器实现保存加密,输出解密,模糊查询

前言&#xff1a;因公司需求需要把某些实体类的某些字段值进行加密保存&#xff0c;在查询时解密明文输出。现记录两种方式。 一、第一种方式&#xff1a; &#xff08;1&#xff09;使用TableField(typeHandler TypeHandler.class)注解自带的字段类型处理器&#xff0c;写一…

电脑死机的时候,CPU到底在做什么?

电脑死机&#xff0c;应该每个接触计算机的小伙伴都经历过吧。 尤其是早些年&#xff0c;电脑配置还没现在这么高的时候&#xff0c;多开几个重量级应用程序&#xff0c;死机就能如约而至&#xff0c;就算你把键盘上的CTRLALTDELETE按烂了&#xff0c;任务管理器也出不来&…

Mybatis-Genertor逆向工程

1、导入mybaties插件 <build><plugins><plugin><groupId>org.mybatis.generator</groupId><artifactId>mybatis-generator-maven-plugin</artifactId><version>1.4.2</version><dependencies><dependency>…

Error: svn: E155004: Run ‘svn cleanup‘ to remove locks

解决办法如下&#xff1a;点击settings 点击清除缓存按钮&#xff0c;然后再使用svn进行提交更新操作&#xff0c;但是可能还会有其它的错误&#xff0c;比如svn: E230001: Server SSL certificate verification failed&#xff0c;解决这个错误请参考我另一篇文章&#xff1a;…

博客系统(升级(Spring))(一)创建数据库,创建实例化对象,统一数据格式,统一报错信息

博客系统&#xff08;一&#xff09; 博客系统一、创建项目二、建立数据库结构链接服务器和数据库和Redis 三、创建实例化对象四、统一数据结构结构 五、统一报错信息 博客系统 博客系统是干什么的&#xff1f; CSDN就是一个典型的博客系统。而我在这里就是通过模拟实现一个博…

Python+Requests+Excel接口测试实战

1、EXCEL文件接口保存方式&#xff0c;如图。 2、然后就是读取EXCEL文件中的数据方法&#xff0c;如下&#xff1a; 1 import xlrd2 3 4 class readExcel(object):5 def __init__(self, path):6 self.path path7 8 property9 def getSheet(self): 10 …

莫比乌斯召回系统介绍

当前召回系统只能召回相关性高的广告&#xff0c;但不能保证该广告变现能力强。莫比乌斯做了如下两点创新&#xff1a; 在召回阶段&#xff0c;引入CPM等业务指标作为召回依据在召回阶段&#xff0c;引入CTR模型&#xff0c;从而召回更多相关性高且变现能力强的广告 参考 百度…

基于Protege的知识建模实战

一.Protege简介、用途和特点 1.Protege简介 Protege是斯坦福大学医学院生物信息研究中心基于Java开发的本体编辑和本体开发工具&#xff0c;也是基于知识的编辑器&#xff0c;属于开放源代码软件。这个软件主要用于语义网中本体的构建&#xff0c;是语义网中本体构建的核心开发…

Elasticsearch:什么是生成式人工智能?

生成式人工智能定义 给学生的解释&#xff08;基本&#xff09;&#xff1a; 生成式人工智能是一种可以创造新的原创内容的技术&#xff0c;例如艺术、音乐、软件代码和写作。 当用户输入提示时&#xff0c;人工智能会根据从互联网上现有示例中学到的知识生成响应&#xff0c;…

linux安装Sentinal1.8.6

前言&#xff1a; 使用docker search sentinel-dashboard命令&#xff0c;发现docker中的镜像版本过低&#xff0c;由于要配合使用1.8.6&#xff0c;所以这里采用java后台运行sentinel1.8.6-jar的方式。 1、官网下载对应版本jar&#xff08;https://github.com/alibaba/Sentin…

MySQL间隙锁深入分析

概念 什么是间隙锁&#xff1f; MySQL的间隙锁&#xff08;gap lock&#xff09;是一种锁定相邻数据间隔的机制。 触发时机&#xff1f; 当使用SELECT…FOR UPDATE或UPDATE语句时&#xff0c;MySQL会获取一个范围锁&#xff0c;包括指定条件内的所有数据行&#xff0c;并且还…

rhcsa4 进程和SSH

tree命令。用于以树状结构显示目录和文件。通过运行 “tree” 命令可视化地查看文件系统中的目录结构。 tree / systemd是第一个系统进程&#xff08;pid1&#xff09;不启动&#xff0c;其他进程也没法启动&#xff0c; 用pstree查看进程树 我们可以看到所有进程都是syste…

设计模式之模板模式

文章目录 豆浆制作问题模板方法模式基本介绍模板方法模式原理类图对原理类图的说明-即(模板方法模式的角色及职责)模板方法模式解决豆浆制作问题模板方法模式的钩子方法模板方法模式的注意事项和细节 豆浆制作问题 编写制作豆浆的程序&#xff0c;说明如下: 制作豆浆的流程 选…

RocketMQ 消息传递模型

文章目录 0. 前言1. RocketMQ的消息传递模型1.1. 同步发送1.2. 异步发送1.3. 单向发送 2. RocketMQ的批量发送和消费2.1 批量发送2.2 批量消费2.3 Spring Boot集成RocketMQ官方starter 示例 3. 总结4. 参考文档5. 源码地址 0. 前言 RocketMQ 支持6种消息传递方式&#xff0c;我…

pyarmor 加密许可证的使用

一 pyarmor 许可证的用处 文档&#xff1a;5. 许可模式和许可证 — Pyarmor 8.3.6 文档 试用版本有如下的限制&#xff1a; 加密功能对脚本大小有限制&#xff0c;不能加密超过限制的大脚本。 混淆字符串功能在试用版中无法使用。 RFT 加密模式&#xff0c;BCC 加密模式在试…

解决java.io.IOException: Network error

解决java.io.IOException: Network error 解决java.io.IOException: Network error摘要引言正文1. 理解异常的根本原因2. 处理网络连接问题3. 处理连接超时4. 处理协议错误或不匹配5. 异常处理 总结参考资料 博主 默语带您 Go to New World. ✍ 个人主页—— 默语 的博客&#…

24.Xaml ListView控件-----显示数据

1.运行效果 2.运行源码 a.Xaml源码 <Window x:Class="testView.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.mic…

电子信息工程专业课复习知识点总结:(四)信号与系统、数字信号处理

这次我不具体把所有概念写出来了&#xff0c;只针对一些面试中经常提问的重点问题。 第一章 信号与系统基本概念 这里提出一个信号与系统这本书的大纲&#xff1a;这本书研究的就是信号与系统的关系。 一.信号是什么&#xff1f; ①信息是自然世界中一种表现形式&#xff0…