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

这篇具有很好参考价值的文章主要介绍了Flutter 中的单元测试:从工作流基础到复杂场景。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Flutter 中的单元测试:从工作流基础到复杂场景,软件测试工程师,软件测试,自动化测试,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 中的单元测试:从工作流基础到复杂场景,软件测试工程师,软件测试,自动化测试,flutter,单元测试,自动化测试,软件测试,功能测试,程序人生,职场发展

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

一个简单的 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 中的单元测试:从工作流基础到复杂场景,软件测试工程师,软件测试,自动化测试,flutter,单元测试,自动化测试,软件测试,功能测试,程序人生,职场发展

在终端,可以通过输入 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 中的单元测试了。

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

Flutter 中的单元测试:从工作流基础到复杂场景,软件测试工程师,软件测试,自动化测试,flutter,单元测试,自动化测试,软件测试,功能测试,程序人生,职场发展

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!   文章来源地址https://www.toymoban.com/news/detail-712549.html

到了这里,关于Flutter 中的单元测试:从工作流基础到复杂场景的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 数据工作流中的安全和隐私保护:技术和最佳实践

    作者:禅与计算机程序设计艺术 随着大数据的爆炸性增长、应用场景日益丰富、用户隐私权保护意识日渐增强等因素的影响,对数据处理过程中的安全和隐私保护问题越来越受到重视。如何充分实现数据集中存储、传输、加工等环节中的安全防护,是一个非常关键的问题。在

    2024年02月14日
    浏览(42)
  • 两小时快速入门 TypeScript 基础(一)工作流、基本类型、高级类型

    个人简介 👀 个人主页: 前端杂货铺 🙋‍♂️ 学习方向: 主攻前端方向,也会涉及到服务端(Node.js 等) 📃 个人状态: 2023届本科毕业生,已拿多个前端 offer(秋招) 🚀 未来打算: 为中国的工业软件事业效力 n 年 🥇 推荐学习:🍍前端面试宝典 🍉Vue2 🍋Vue3 🍓Vue2/

    2024年02月11日
    浏览(35)
  • PowerAutomate工作流实现将Sharepoint列表中的数据复制到任意站点的某个列表

            本次的工作流案例是实现将Sharepoint列表中的数据复制到任意站点的列表,要实现的目标是:当列表A中创建或修改数据时,工作流会获取该数据并复制到列表B中,同样列表A中的数据发生修改时,列表B中存在的数据同步发生修改。 下面以两个列表进行举例,列表

    2024年02月11日
    浏览(35)
  • 【工作流】Activiti工作流简介以及Spring Boot 集成 Activiti7

    什么是工作流? 工作流指通过计算机对业务流程进行自动化管理,实现多个参与者按照预定义的流程去自动执行业务流程。 文章源码托管:https://github.com/OUYANGSIHAI/Activiti-learninig Activiti5是由Alfresco软件在2010年5月17日发布的业务流程管理(BPM)框架,它是覆盖了业务流程管理、

    2024年02月08日
    浏览(43)
  • 云原生离线工作流编排利器 -- 分布式工作流 Argo 集群

    作者:庄宇 在现代的软件开发和数据处理领域,批处理作业(Batch)扮演着重要的角色。它们通常用于数据处理,仿真计算,科学计算等领域,往往需要大规模的计算资源。随着云计算的兴起,阿里云批量计算和 AWS Batch 等云服务提供了管理和运行这些批处理作业的平台。 随

    2024年01月24日
    浏览(73)
  • Camunda 7工作流引擎 API 以及与Springboot集成实现工作流配置全纪录

    项目中需要用到工作流引擎来设计部分业务流程,框架选型最终选择了 Camunda7,关于 Camunda以及 Activity 等其他工作流 引擎的介绍及对比不再介绍,这里只介绍与现有Springboot项目的集成以及具体使用及配置 流程(PROCESS): 通过工具建模最终生成的BPMN文件,里面有整个流程的定

    2024年02月10日
    浏览(48)
  • Git 工作流设计

    前言 常用的工作流有四种 集中式工作流 功能分支流 git flow 工作流 forking 工作流 集中式工作流 集中式工作流,多个功能(feat),bug修复(fix) 在一个分支上开发,极容易出现代码从冲突 功能分支流 新的功能或者bug fork出一个新的分支,在该分支上开发 功能在分支开发完后再合

    2024年02月05日
    浏览(53)
  • 工作流引擎Flowable

    官方手册 一、依赖 二、demo 三、日志文件 在resources中添加日志文件log4j.properties Flowable流程图 Eclipse Designer, 一款Eclipse插件, 用于图形化建模, 测试与部署BPMN2.0流程 FlowableUI Flowable BPMN visualizer, 一款idea插件 从官网下载flowable-6.7.2.zip解压后, 可以看到如下两个文件 将这两个文件

    2024年02月09日
    浏览(45)
  • Git工作流

    main:生产环境,也就是你们在网上可以下载到的版本,是经过了很多轮测试得到的稳定版本。 release: 开发内部发版,也就是测试环境。 dev:所有的feature都要从dev上checkout。 feature:每个需求新创建的分支。 下面介绍一下一个新需求过来的git操作流程: 1.从dev分支上checkou

    2024年02月10日
    浏览(37)
  • Git工作流(随笔)

    目录 前言 一、工作流概述 1、概念 2、分类 二、集中式工作流 1、概述 2、介绍 3、操作过程 三、功能分支工作流 1、概述 2、介绍 3、操作过程 1)创建远程分支 2)删除远程分支 四、GitFlow工作流 1、概述 2、介绍   3、操作过程 五、Forking工作流 1、概述 2、介绍 3、操作过程

    2024年02月09日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包