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

2023-11-20

对 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 declarations

        group("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 declarations

        group("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);


  @override

  void close({bool force = false}) {}


  @override

  Future<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 中的单元测试了。

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

在这里插入图片描述

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

随机推荐

  • ST公司 Lis2dh12 三轴加速度传感器,计算加速度值转成角度值

    目录 概述 项目上使用了一款Lis2dh12三轴加速度传感器 开发前要准备的工作 1 原理图 1 1 创建lis2dh12 c文件 1 2 在此重点说明 如果想调传感器的中断灵敏度 注意 关注1 INT1 THS 32h 2 INT1 DU
  • 华为OD机试真题- 任务混部【2023Q1】【JAVA、Python、C++】

    题目描述 公司创新实验室正在研究如何最小化资源成本 最大化资源利用率 请你设计算法帮他们解决一个任务混部问题 有taskNum项任务 每个任务有开始时间 startTime 结束时间 endTime 并行度 parallelism 三个属性
  • 什么是分布式架构

    一 分布式架构定义 什么是分布式架构 分布式系统 distributed system 是建立在网络之上的软件系统 内聚性 是指每一个数据库分布节点高度自治 有本地的数据库管理系统 透明性 是指每一个数据库分布节点对用户的应用来说都是透明的
  • 多处理器编程的艺术(二)-并行程序设计

    当处理器的性能的发展受到各方面因素的限制的时候 计算机产业开始用多处理器结构实现并行计算来提高计算的效率 我们使用多处理器共享存储器的方式实现了多处理器编程 也就是多核编程 当然在这样的系统结构下我们面临着各种各样的挑战 例如如何协调各个处
  • 十四、java版 SpringCloud分布式微服务云架构之Java String 类

    Java String 类 字符串广泛应用 在 Java 编程中 在 Java 中字符串属于对象 Java 提供了 String 类来创建和操作字符串 创建字符串 创建字符串最简单的方式如下 String str xxx 在代码中遇到字符串
  • STM32串口中断、DMA接收的几点注意地方

    STM32串口中断 DMA接收的几点注意地方 https tieba baidu com p 5978431198 red tag 1717231177 traceid 这个文章棒 今天写点大家常问 也是常见的关于UART串口的内容 这几点
  • Ubuntu 18.04安装教程(转)

    https blog csdn net qq 39478237 article details 83084515 参考这个播客安装
  • LAB1实验

    Part 1 遇到问题1 我将JOS放在Windows的目录下 通过VMware设置共享该文件夹来编译JOS 但是Windows更改linux下设置的权限 导致GDB无法调试QEMU 解决方法 将JOS放在虚拟机下的linux的目录下 20
  • Activiti6.0学习实践(5)-activiti流程引擎配置二

    本节继续对activiti流程引擎的配置进行学习 1 EventLog配置 1 1 配置EventLog 首先在activiti eventlog cfg xml中配置eventlog属性为true 1 1 1测试代码 编写一个eventl
  • 推荐111个软件工程本科的计算机毕业设计,有手就会

    对于即将挑战计算机专业毕业设计的学子们 是否已经为选题和项目感到焦虑 今天 我们为即将毕业的学生提供了大量的毕业设计项目 期望对于正在为毕业设计挠头的同学们有所助益 一 成品列表 以下所有springboot框架项目的源码博主已经打包好上传
  • Redis介绍、安装、基础命令

    目录 引言 一 关系数据库和非关系数据库 1 1 关系型数据库 1 2 非关系型数据库 1 3 关系型数据库与非关系型数据库区别 数据存储方式不同 扩展方式不同 对事务性的支持不同 非关系型数据库产生背景 二 Redis简介 2 1 Red
  • 网站打开速度缓慢的原因都有哪些?

    在进行站点优化时 很多站长会发现我们的网站有时运行速度很快 有时运行速度很慢 严重影响了用户体验 因此 有必要理解为什么网站变得很慢 如今 可以帮助你了解为什么我们的网站会慢下来 1 网页的大小 网页加载速度与网页大小直接相关 站点的代码文
  • 2023 最新版IntelliJ IDEA 2023.1创建Java Web 项目详细步骤(图文详解)

    前言 本篇文章仅作为刚开始使用 IntelliJ IDEA 2023 1 创建一个简单的web项目的开发人员 只是作为入门使用 目前很多都是使用spring boot框架来搭建Java的web项目 但是spring boot的最新版本目前
  • C#System.ArgumentException

    C 自定义控件GDI绘制在主程序报错System ArgumentException 我的绘制图片内容大概如下 private Bitmap backGroundImage null private Bitmap prospectImage
  • Java 6-1 项目模块化-概念

    6 1 项目模块化 概念 一 组件化与模块化 组件化 以功能为依据 解决复用问题 初衷 可复用的代码 进行工具性的封装 目的 复用 解耦 依赖 各组件之间独立 低依赖甚至零依赖 架构 纵向 位于项目底层 被其他上层依赖 举例 Dialog
  • 完全数

    my0163 完全数 HOBO浩 描述 求正整数 2 和 n 之间的完全数 一行一个数 完全数 因子之和等于它本身的自然数 如 6 1 2 3 输入 输入n 1 n 5000 输出 一行一个数 按由小到大的顺序 输入样例 7 输出样例 6
  • 自学网络安全(黑客)的误区

    前言 网络安全入门到底是先学编程还是先学计算机基础 这是一个争议比较大的问题 有的人会建议先学编程 而有的人会建议先学计算机基础 其实这都是要学的 而且这些对学习网络安全来说非常重要 一 网络安全学习的误区 1 不要试图以编程为基础去学习网
  • java 二阶段提交,二阶段提交协议(Two Phase Commitment Protocol)

    一 典型的分布式事务实例 跨行转账问题是一个典型的分布式事务 用户A向B的一个转账1000 要进行A的余额 1000 B的余额 1000 显然必须保证这两个操作的事务性 类似的还有 电商系统中 当有用户下单后 除了在订单表插入记 还要在商品
  • mysql数据库常用sql语句

    数据库可以用图形化工具来实现一系列操作 这里涉及一些cmd命令行 首先要配置好环境变量可以全局操作命令 不然只能在mysql的安装目录下进行操作 这里不再叙述 1 进入数据库 mysql u root p 默认用户名为root 这个与mys
  • Flutter 中的单元测试:从工作流基础到复杂场景

    对 Flutter 的兴趣空前高涨 而且早就应该出现了 Google 的开源 SDK 与 Android iOS macOS Web Windows 和 Linux 兼容 单个 Flutter 代码库支持所有这些 单元测试有助于交付一致且可