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

打印 上一主题 下一主题

主题 1015|帖子 1015|积分 3045

对 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 对象中运行此函数后的预期结果:
  1. void main() {
  2.     Map<String, dynamic> apiUniversityOneAsJson = {
  3.         "alpha_two_code": "US",
  4.         "domains": ["marywood.edu"],
  5.         "country": "United States",
  6.         "state-province": null,
  7.         "web_pages": ["http://www.marywood.edu"],
  8.         "name": "Marywood University"
  9.     };
  10.     ApiUniversityModel expectedApiUniversityOne = ApiUniversityModel(
  11.         alphaCode: "US",
  12.         country: "United States",
  13.         state: null,
  14.         name: "Marywood University",
  15.         websites: ["http://www.marywood.edu"],
  16.         domains: ["marywood.edu"],
  17.     );
  18.     University expectedUniversityOne = University(
  19.         alphaCode: "US",
  20.         country: "United States",
  21.         state: "",
  22.         name: "Marywood University",
  23.         websites: ["http://www.marywood.edu"],
  24.         domains: ["marywood.edu"],
  25.     );
  26.     Map<String, dynamic> apiUniversityTwoAsJson = {
  27.         "alpha_two_code": "US",
  28.         "domains": ["lindenwood.edu"],
  29.         "country": "United States",
  30.         "state-province":"MJ",
  31.         "web_pages": null,
  32.         "name": "Lindenwood University"
  33.     };
  34.     ApiUniversityModel expectedApiUniversityTwo = ApiUniversityModel(
  35.         alphaCode: "US",
  36.         country: "United States",
  37.         state:"MJ",
  38.         name: "Lindenwood University",
  39.         websites: null,
  40.         domains: ["lindenwood.edu"],
  41.     );
  42.     University expectedUniversityTwo = University(
  43.         alphaCode: "US",
  44.         country: "United States",
  45.         state: "MJ",
  46.         name: "Lindenwood University",
  47.         websites: [],
  48.         domains: ["lindenwood.edu"],
  49.     );
  50. }
复制代码
接下来,第三个和第四个目的,将添加形貌性语言来定义测试组和测试函数签名:
  1.    void main() {
  2.     // Previous declarations
  3.         group("Test ApiUniversityModel initialization from JSON", () {
  4.             test('Test using json one', () {});
  5.             test('Test using json two', () {});
  6.         });
  7.         group("Test ApiUniversityModel toDomain", () {
  8.             test('Test toDomain using json one', () {});
  9.             test('Test toDomain using json two', () {});
  10.         });
  11. }
复制代码
现在定义了两个测试的签名来查抄 fromJson 函数,两个测试来查抄 toDomain函数。
为了实现第五个目的并编写测试,将利用 flutter_test库的 expect 方法将函数的结果与预期进行比较:
  1. void main() {
  2.     // Previous declarations
  3.         group("Test ApiUniversityModel initialization from json", () {
  4.             test('Test using json one', () {
  5.                 expect(ApiUniversityModel.fromJson(apiUniversityOneAsJson),
  6.                     expectedApiUniversityOne);
  7.             });
  8.             test('Test using json two', () {
  9.                 expect(ApiUniversityModel.fromJson(apiUniversityTwoAsJson),
  10.                     expectedApiUniversityTwo);
  11.             });
  12.         });
  13.         group("Test ApiUniversityModel toDomain", () {
  14.             test('Test toDomain using json one', () {
  15.                 expect(ApiUniversityModel.fromJson(apiUniversityOneAsJson).toDomain(),
  16.                     expectedUniversityOne);
  17.             });
  18.             test('Test toDomain using json two', () {
  19.                 expect(ApiUniversityModel.fromJson(apiUniversityTwoAsJson).toDomain(),
  20.                     expectedUniversityTwo);
  21.             });
  22.         });
  23. }
复制代码
完成五个目的后,现在可以从 IDE 或命令行运行测试。

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

完成了一个没有依赖项的简朴测试后,让我们探索一个更风趣的示例:将测试endpoint类,其范围包罗:
●执行对服务器的 API 调用。
●将 API JSON 响应转换为不同的格式。
在评估了代码之后,将利用 flutter_test库的 setUp方法来初始化测试组中的类:
  1. group("Test University Endpoint API calls", () {
  2.     setUp(() {
  3.         baseUrl = "https://test.url";
  4.         dioClient = Dio(BaseOptions());
  5.         endpoint = UniversityEndpoint(dioClient, baseUrl: baseUrl);
  6.     });
  7. }
复制代码
要向 API 发出网络请求,更喜欢利用改造库,它会天生大部分必要的代码。 为了正确测试 UniversityEndpoint类,将欺压 dio 库(Retrofit 用于执行 API 调用)通过自定义响应适配器模拟 Dio 类的行为来返回所需的结果。
自定义网络拦截器

由于通过 DI 构建了UniversityEndpoint类,因此可以进行自定义网络拦截器。 (假如 UniversityEndpoint 类自己初始化一个 Dio 类,就没有办法模拟类的行为。)
为了模拟Dio类的行为,必要知道 Retrofit库中利用的 Dio方法—— 但无法直接访问 Dio。 因此,将利用自定义网络响应拦截器模拟 Dio:
  1. class DioMockResponsesAdapter extends HttpClientAdapter {
  2.   final MockAdapterInterceptor interceptor;
  3.   DioMockResponsesAdapter(this.interceptor);
  4.   @override
  5.   void close({bool force = false}) {}
  6.   @override
  7.   Future<ResponseBody> fetch(RequestOptions options,
  8.       Stream<Uint8List>? requestStream, Future? cancelFuture) {
  9.     if (options.method == interceptor.type.name.toUpperCase() &&
  10.         options.baseUrl == interceptor.uri &&
  11.         options.queryParameters.hasSameElementsAs(interceptor.query) &&
  12.         options.path == interceptor.path) {
  13.       return Future.value(ResponseBody.fromString(
  14.         jsonEncode(interceptor.serializableResponse),
  15.         interceptor.responseCode,
  16.         headers: {
  17.           "content-type": ["application/json"]
  18.         },
  19.       ));
  20.     }
  21.     return Future.value(ResponseBody.fromString(
  22.         jsonEncode(
  23.               {"error": "Request doesn't match the mock interceptor details!"}),
  24.         -1,
  25.         statusMessage: "Request doesn't match the mock interceptor details!"));
  26.   }
  27. }
  28. enum RequestType { GET, POST, PUT, PATCH, DELETE }
  29. class MockAdapterInterceptor {
  30.   final RequestType type;
  31.   final String uri;
  32.   final String path;
  33.   final Map<String, dynamic> query;
  34.   final Object serializableResponse;
  35.   final int responseCode;
  36.   MockAdapterInterceptor(this.type, this.uri, this.path, this.query,
  37.       this.serializableResponse, this.responseCode);
  38. }
复制代码
现在已经创建了拦截器来模拟网络响应,接下来可以定义测试组和测试函数签名。在例子中,只有一个函数要测试 (getUniversitiesByCountry),因此将只创建一个测试组。现测试函数对三种环境的响应:
1.Dio类的函数是否真的被 getUniversitiesByCountry 调用了?
2.假如API 请求返回错误,会发生什么?
3.假如 API 请求返回预期结果,会发生什么?
这是测试组和测试函数签名:
  1. group("Test University Endpoint API calls", () {
  2.     test('Test endpoint calls dio', () async {});
  3.     test('Test endpoint returns error', () async {});
  4.     test('Test endpoint calls and returns 2 valid universities', () async {});
  5.   });
复制代码
现在预备好编写测试用例了。对于每个测试用例,要创建一个具有相应设置的 DioMockResponsesAdapter 实例:
  1. group("Test University Endpoint API calls", () {
  2.     setUp(() {
  3.         baseUrl = "https://test.url";
  4.         dioClient = Dio(BaseOptions());
  5.         endpoint = UniversityEndpoint(dioClient, baseUrl: baseUrl);
  6.     });
  7.     test('Test endpoint calls dio', () async {
  8.         dioClient.httpClientAdapter = _createMockAdapterForSearchRequest(
  9.             200,
  10.             [],
  11.         );
  12.         var result = await endpoint.getUniversitiesByCountry("us");
  13.         expect(result, <ApiUniversityModel>[]);
  14.     });
  15.     test('Test endpoint returns error', () async {
  16.         dioClient.httpClientAdapter = _createMockAdapterForSearchRequest(
  17.             404,
  18.             {"error": "Not found!"},
  19.         );
  20.         List<ApiUniversityModel>? response;
  21.         DioError? error;
  22.         try {
  23.             response = await endpoint.getUniversitiesByCountry("us");
  24.         } on DioError catch (dioError, _) {
  25.             error = dioError;
  26.         }
  27.         expect(response, null);
  28.         expect(error?.error, "Http status error [404]");
  29.     });
  30.     test('Test endpoint calls and returns 2 valid universities', () async {
  31.         dioClient.httpClientAdapter = _createMockAdapterForSearchRequest(
  32.             200,
  33.             generateTwoValidUniversities(),
  34.         );
  35.         var result = await endpoint.getUniversitiesByCountry("us");
  36.         expect(result, expectedTwoValidUniversities());
  37.     });
  38. });
复制代码
现在端点测试已经完成,开始测试数据源类 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:
  1. import 'university_remote_data_source_test.mocks.dart';
  2. @GenerateMocks([UniversityEndpoint])
  3. void main() {
  4.     late UniversityEndpoint endpoint;
  5.     late UniversityRemoteDataSource dataSource;
  6.     group("Test function calls", () {
  7.         setUp(() {
  8.             endpoint = MockUniversityEndpoint();
  9.             dataSource = UniversityRemoteDataSource(universityEndpoint: endpoint);
  10.         });
  11. }
复制代码
乐成模拟了UniversityEndpoint,然后初始化了UniversityRemoteDataSource 类。 现在预备好定义测试组和测试函数签名:
  1. group("Test function calls", () {
  2.   test('Test dataSource calls getUniversitiesByCountry from endpoint', () {});
  3.   test('Test dataSource maps getUniversitiesByCountry response to Stream', () {});
  4.   test('Test dataSource maps getUniversitiesByCountry response to Stream with error', () {});
  5. });
复制代码
这样,模拟、测试组和测试函数签名就设置好了。 已预备好编写现实测试。
第一个测试查抄当数据源启动国家信息获取时是否调用了 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 函数:
  1. when(endpoint.getUniversitiesByCountry("test"))
  2.     .thenAnswer((realInvocation) => Future.value(<ApiUniversityModel>[]));
复制代码
现在已经模拟了响应,调用数据源函数并利用验证函数查抄是否调用了UniversityEndpoint函数:
  1. test('Test dataSource calls getUniversitiesByCountry from endpoint', () {
  2.     when(endpoint.getUniversitiesByCountry("test"))
  3.         .thenAnswer((realInvocation) => Future.value(<ApiUniversityModel>[]));
  4.     dataSource.getUniversitiesByCountry("test");
  5.     verify(endpoint.getUniversitiesByCountry("test"));
  6. });
复制代码
可以利用相同的原则来编写额外的测试来查抄函数是否正确地将端点结果转换为相关的数据流:
  1. import 'university_remote_data_source_test.mocks.dart';
  2. @GenerateMocks([UniversityEndpoint])
  3. void main() {
  4.     late UniversityEndpoint endpoint;
  5.     late UniversityRemoteDataSource dataSource;
  6.     group("Test function calls", () {
  7.         setUp(() {
  8.             endpoint = MockUniversityEndpoint();
  9.             dataSource = UniversityRemoteDataSource(universityEndpoint: endpoint);
  10.         });
  11.         test('Test dataSource calls getUniversitiesByCountry from endpoint', () {
  12.             when(endpoint.getUniversitiesByCountry("test"))
  13.                     .thenAnswer((realInvocation) => Future.value(<ApiUniversityModel>[]));
  14.             dataSource.getUniversitiesByCountry("test");
  15.             verify(endpoint.getUniversitiesByCountry("test"));
  16.         });
  17.         test('Test dataSource maps getUniversitiesByCountry response to Stream',
  18.                 () {
  19.             when(endpoint.getUniversitiesByCountry("test"))
  20.                     .thenAnswer((realInvocation) => Future.value(<ApiUniversityModel>[]));
  21.             expect(
  22.                 dataSource.getUniversitiesByCountry("test"),
  23.                 emitsInOrder([
  24.                     const AppResult<List<University>>.loading(),
  25.                     const AppResult<List<University>>.data([])
  26.                 ]),
  27.             );
  28.         });
  29.         test(
  30.                 'Test dataSource maps getUniversitiesByCountry response to Stream with error',
  31.                 () {
  32.             ApiError mockApiError = ApiError(
  33.                 statusCode: 400,
  34.                 message: "error",
  35.                 errors: null,
  36.             );
  37.             when(endpoint.getUniversitiesByCountry("test"))
  38.                     .thenAnswer((realInvocation) => Future.error(mockApiError));
  39.             expect(
  40.                 dataSource.getUniversitiesByCountry("test"),
  41.                 emitsInOrder([
  42.                     const AppResult<List<University>>.loading(),
  43.                     AppResult<List<University>>.apiError(mockApiError)
  44.                 ]),
  45.             );
  46.         });
  47.     });
  48. }
复制代码
我们已经执行了许多 Flutter 单元测试并演示了不同的模拟方法。 可以继续利用示例Flutter 项目来运行其他测试。
Flutter 单元测试:实现卓越用户体验的关键

假如已经将单元测试整合到 Flutter 项目中,本文可能已经先容了一些可以注入到工作流程中的新选项。 在本教程中,演示了将单元测试合并到下一个 Flutter 项目中是多么简朴,以及如何应对更细微的测试场景的挑战。你可能再也不想跳过 Flutter 中的单元测试了。
最后: 下方这份完备的软件测试视频学习教程已经整理上传完成,朋侪们假如必要可以自行免费领取 【包管100%免费】

这些资料,对于【软件测试】的朋侪来说应该是最全面最完备的备战仓库,这个仓库也伴随上万个测试工程师们走过最艰巨的路程,渴望也能资助到你!
  1. 软件测试技术交流群社:746506216(里面还有工作内推机会,毕竟我们是关系社会。)
复制代码
软件测试面试文档

我们学习肯定是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有字节大佬给出了权势巨子的解答,刷完这一套面试资料相信大家都能找到满足的工作。

面试文档获取方式:




免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

没腿的鸟

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表