使用 Mockito 对 Flutter 代码进行单元测试

这篇具有很好参考价值的文章主要介绍了使用 Mockito 对 Flutter 代码进行单元测试。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

单元测试验证单个方法或类是否按预期工作。它还通过在进行新更改时确认现有逻辑是否仍然有效来提高可维护性。

通常,单元测试很容易编写,但可以在测试环境中运行。400默认情况下,这会在进行网络调用或 HTTP 请求时产生带有状态代码的空响应。为了解决这个问题,我们可以在每次发出 HTTP 请求时轻松使用 Mockito 返回虚假响应。Mockito 有各种用例,随着我们的进行,我们将逐步介绍这些用例。

在本教程中,我们将演示如何使用 Mockito 来测试 Flutter 代码。我们将学习如何生成模拟、存根数据以及对发出流的方法执行测试。让我们开始吧!

  • 什么是 Mockito?

  • 生成模拟和存根数据

    • 项目结构概述

    • 依赖注入

  • 使用参数匹配器

  • 在 Mockito 中创建假货

  • 在 Flutter 中模拟和测试流

什么是 Mockito?

Mockito 是一个众所周知的包,它可以更轻松地生成现有类的假实现。它消除了重复编写这些功能的压力。此外,Mockito 有助于控制输入,因此我们可以测试预期的结果。

假设使用 Mockito 可以更轻松地编写单元测试,但是,如果架构不好,模拟和编写单元测试很容易变得复杂。

在本教程的后面,我们将学习如何将 Mockito 与模型-视图-视图模型 (MVVM) 模式一起使用,该模式涉及将代码库分成不同的可测试部分,例如视图模型和存储库。

生成模拟和存根数据

模拟是真实类的假实现。它们通常用于控制测试的预期结果,或者当真实类在测试环境中容易出错时。

为了更好地理解这一点,我们将为处理发送和接收帖子的应用程序编写单元测试。

项目结构概述

在开始之前,让我们将所有必要的包添加到我们的项目中。

dependencies:
  dio: ^4.0.6 # For making HTTP requests
​
dev_dependencies:
  build_runner: ^2.2.0 # For generating code (Mocks, etc)
  mockito: ^5.2.0 # For mocking and stubbing

我们将使用 MVVM 和存储库模式,其中包括对存储库和视图模型的测试。在 Flutter 中,将所有测试放在test文件夹中是一个很好的做法,它与文件夹的结构非常匹配lib。

接下来,我们将通过附加到文件名来创建authentication_repository.dart和文件。这有助于测试运行者找到项目中存在的所有测试。authentication_repository_test.dart``_test


超过 20 万开发人员使用 LogRocket 来创造更好的数字体验了解更多 →


我们将通过创建一个名为AuthRepository. 顾名思义,这个类将处理我们应用程序中的所有身份验证功能。之后,我们将包含一个登录方法,该方法检查状态代码是否相等200并捕获身份验证时发生的任何错误。

class AuthRepository {
  Dio dio = Dio();
​
  AuthRepository();
​
  Future<bool> login({
    required String email,
    required String password,
  }) async {
    try {
      final result = await dio.post(
        '<https://reqres.in/api/login>',
        data: {'email': email, 'password': password},
      );
​
      if (result.statusCode != 200) {
        return false;
      }
    } on DioError catch (e) {
      print(e.message);
      return false;
    }
​
    return true;
  }
​
  // ...
}
void main() {
  late AuthRepository authRepository;
​
  setUp(() {
    authRepository = AuthRepository();
  });
​
  test('Successfully logged in user', () async {
    expect(
      await authRepository.login(email: 'james@mail.com', password: '123456'),
      true,
    );
  });
}

在上面的测试中,我们AuthRepository在 setup 函数中初始化了 。由于它将在每个测试和测试组之前直接在内部运行main,因此它将auth为每个测试或组初始化一个新的存储库。

接下来,我们将编写一个测试,期望登录方法返回true而不会抛出错误。但是,测试仍然失败,因为单元测试默认不支持发出网络请求,因此发出的登录请求Dio返回状态码400。

为了解决这个问题,我们可以使用 Mockito 生成一个模拟类,其功能类似于Dio. 在 Mockito 中,我们通过@GenerateMocks([classes])在方法的开头添加注释来生成模拟main。这会通知构建运行器为列表中的所有类生成模拟。

@GenerateMocks([Dio, OtherClass])
void main(){
    // test for login
}

接下来,打开终端并运行命令flutter pub run build_runner build以开始为类生成模拟。代码生成完成后,我们将能够通过添加Mock类名来访问生成的模拟。

@GenerateMocks([Dio])
void main(){
      MockDio mockDio = MockDio()
      late AuthRepository authRepository;
      ...
}

我们必须对数据进行存根,以确保MockDio在调用登录端点时返回正确的响应数据。在 Flutter 中,存根意味着在调用 mock 方法时返回一个假对象。例如,当测试使用 调用登录端点时MockDio,我们应该返回一个带有状态码的响应对象200。

可以使用 function 来对 mock 进行存根,该 functionwhen()可以与thenReturn,一起使用thenAnswer,或者thenThrow在我们调用 mock 方法时提供所需的值。该thenAnswer函数用于返回未来或流的方法,而thenReturn用于模拟类的普通同步方法。


// To stub any method; gives error when used for futures or stream
when(mock.method()).thenReturn(value);
​
// To stub method that return a future or stream
when(mock.method()).thenAnswer(() => futureOrStream);
​
// To stub error
when(mock.method()).thenThrow(errorObject);
​
// dart
@GenerateMocks([Dio])
void main() {
  MockDio mockDio = MockDio();
  late AuthRepository authRepository;
​
  setUp(() {
    authRepository = AuthRepository();
  });
​
  test('Successfully logged in user', () async {
    // Stubbing
    when(mockDio.post(
      '<https://reqres.in/api/login>',
      data: {'email': 'james@mail.com', 'password': '123456'},
    )).thenAnswer(
      (inv) => Future.value(Response(
        statusCode: 200,
        data: {'token': 'ASjwweiBE'},
        requestOptions: RequestOptions(path: '<https://reqres.in/api/login>'),
      )),
    );
​
    expect(
      await authRepository.login(email: 'james@mail.com', password: '123456'),
      true,
    );
  });
}

创建存根后,我们仍然需要传入MockDio测试文件,以便使用它而不是真正的dio类。为了实现这一点,我们将从中删除真实dio类的定义或实例化,authRepository并允许它通过其构造函数传递。这个概念称为依赖注入。

依赖注入

Flutter 中的依赖注入是一种技术,其中一个对象或类提供另一个对象的依赖项。这种模式确保测试模型和视图模型都可以定义dio它们想要使用的类型。

class AuthenticationRepository{
        Dio dio;
​
        // Instead of specifying the type of dio to be used
        // we let the test or viewmodel define it
        AuthenticationRepository(this.dio)
}
@GenerateMocks([Dio])
void main() {
  MockDio mockDio = MockDio();
  late AuthRepository authRepository;
​
  setUp(() {
    // we can now pass in Dio as an argument
    authRepository = AuthRepository(mockDio);
  });
}

使用参数匹配器

在前面的登录示例中,如果在发出请求时james@mail.com更改了电子邮件sam@mail.com,则测试将产生no stub found错误。这是因为我们只为james@mail.com.

但是,在大多数情况下,我们希望通过使用 Mockito 提供的参数匹配器来避免重复不必要的逻辑。使用参数匹配器,我们可以将相同的存根用于广泛的值而不是确切的类型。

为了更好地理解匹配参数,我们将测试PostViewModel并为PostRepository. 建议使用这种方法,因为当我们存根时,我们将返回自定义对象或模型,而不是响应和映射。这也很容易!

首先,我们将创建PostModel更清晰地表示数据的 。

class PostModel {
  PostModel({
    required this.id,
    required this.userId,
    required this.body,
    required this.title,
  });
​
  final int id;
  final String userId;
  final String body;
  final String title;
​
  // implement fromJson and toJson methods for this
}

接下来,我们创建PostViewModel. 这用于检索或发送数据到PostRepository. PostViewModel只是从存储库中发送和检索数据并通知 UI 使用新数据重建。

import 'package:flutter/material.dart';
import 'package:mockito_article/models/post_model.dart';
import 'package:mockito_article/repositories/post_repository.dart';
​
class PostViewModel extends ChangeNotifier {
  PostRepository postRepository;
  bool isLoading = false;
​
  final Map<int, PostModel> postMap = {};
​
  PostViewModel(this.postRepository);
​
  Future<void> sharePost({
    required int userId,
    required String title,
    required String body,
  }) async {
    isLoading = true;
    await postRepository.sharePost(
      userId: userId,
      title: title,
      body: body,
    );
​
    isLoading = false;
    notifyListeners();
  }
​
  Future<void> updatePost({
    required int userId,
    required int postId,
    required String body,
  }) async {
    isLoading = true;
    await postRepository.updatePost(postId, body);
​
    isLoading = false;
    notifyListeners();
  }
​
  Future<void> deletePost(int id) async {
    isLoading = true;
    await postRepository.deletePost(id);
​
    isLoading = false;
    notifyListeners();
  }
​
  Future<void> getAllPosts() async {
    isLoading = true;
    final postList = await postRepository.getAllPosts();
​
    for (var post in postList) {
      postMap[post.id] = post;
    }
​
    isLoading = false;
    notifyListeners();
  }
}

如前所述,我们模拟依赖关系而不是我们测试的实际类。在这个例子中,我们为 . 编写单元测试PostViewModel并模拟PostRepository. 这意味着我们将调用生成MockPostRepository类中的方法,而不是PostRepository可能引发错误的方法。

Mockito 使匹配参数变得非常容易。例如,看updatePost一下PostViewModel. 它调用存储库updatePost方法,该方法只接受两个位置参数。对于这个类方法的存根,我们可以选择提供精确的postIdand body,或者我们可以使用anyMockito 提供的变量来保持简单。

@GenerateMocks([PostRepository])
void main() {
  MockPostRepository mockPostRepository = MockPostRepository();
  late PostViewModel postViewModel;

  setUp(() {
    postViewModel = PostViewModel(mockPostRepository);
  });

  test('Updated post successfully', () {
    // stubbing with argument matchers and 'any'
    when(
      mockPostRepository.updatePost(any, argThat(contains('stub'))),
    ).thenAnswer(
      (inv) => Future.value(),
    );

    // This method calls the mockPostRepository update method
    postViewModel.updatePost(
      userId: 1,
      postId: 3,
      body: 'include `stub` to receive the stub',
    );

    // verify the mock repository was called
    verify(mockPostRepository.updatePost(3, 'include `stub` to receive the stub'));
  });
}

上面的存根包括any变量和argThat(matcher)函数。在 Dart 中,匹配器用于指定测试期望。我们有不同类型的匹配器适用于不同的测试用例。例如,如果对象包含相应的值,则匹配器contains(value)返回。true

匹配位置参数和命名参数

在 Dart 中,我们也有位置参数和命名参数。在上面的示例中,方法的模拟和存根updatePost处理位置参数并使用any变量。

但是,命名参数不支持any变量,因为 Dart 没有提供一种机制来知道元素是否用作命名参数。相反,我们anyNamed(’name’)在处理命名参数时使用该函数。

when(
  mockPostRepository.sharePost(
    body: argThat(startsWith('stub'), named: 'body'),
    postId: anyNamed('postId'),
    title: anyNamed('title'),
    userId: 3,
  ),
).thenAnswer(
  (inv) => Future.value(),
);

当使用带有命名参数的匹配器时,我们必须提供参数的名称以避免错误。您可以在 Dart 文档中阅读有关匹配器的更多信息,以查看所有可能的可用选项。

向日葵远程控制软件,居家办公必备神器,支持手机控制电脑远程传输文件!

在 Mockito 中创建假货

模拟和假货经常被混淆,所以让我们快速澄清两者之间的区别。

模拟是生成的类,允许使用参数匹配器进行存根。然而,Fake 是覆盖真实类的现有方法以提供更大灵活性的类,所有这些都无需使用参数匹配器。

例如,在 post 存储库中使用 fakes 而不是 mocks 将允许我们使 fake repository 功能类似于真实存储库。这是可能的,因为我们能够根据提供的值返回结果。简单来说,当我们调用sharePost测试时,我们可以选择保存帖子,稍后再确认帖子是否被保存getAllPosts。

class FakePostRepository extends Fake implements PostRepository {
  Map<int, PostModel> fakePostStore = {};

  @override
  Future<PostModel> sharePost({
    int? postId,
    required int userId,
    required String title,
    required String body,
  }) async {
    final post = PostModel(
      id: postId ?? 0,
      userId: userId,
      body: body,
      title: title,
    );
    fakePostStore[postId ?? 0] = post;
    return post;
  }

  @override
  Future<void> updatePost(int postId, String body) async {
    fakePostStore[postId] = fakePostStore[postId]!.copyWith(body: body);
  }

  @override
  Future<List<PostModel>> getAllPosts() async {
    return fakePostStore.values.toList();
  }

  @override
  Future<bool> deletePost(int id) async {
    fakePostStore.remove(id);

    return true;
  }
}

更新后的测试使用fake如下所示。使用fake,我们可以一次测试所有方法。帖子在添加或共享时将获取到存储库中的地图。

@GenerateMocks([PostRepository])
void main() {
  FakePostRepository fakePostRepository = FakePostRepository();
  late PostViewModel postViewModel;

  setUp(() {
    postViewModel = PostViewModel(fakePostRepository);
  });

  test('Updated post successfully', () async {
    expect(postViewModel.postMap.isEmpty, true);
    const postId = 123;

    postViewModel.sharePost(
      postId: postId,
      userId: 1,
      title: 'First Post',
      body: 'My first post',
    );
    await postViewModel.getAllPosts();
    expect(postViewModel.postMap[postId]?.body, 'My first post');

    postViewModel.updatePost(
      postId: postId,
      userId: 1,
      body: 'My updated post',
    );
    await postViewModel.getAllPosts();
    expect(postViewModel.postMap[postId]?.body, 'My updated post');
  });
}

在 Flutter 中模拟和测试流

使用 Mockito 模拟和存根流与期货非常相似,因为我们对存根使用相同的语法。然而,流与期货有很大不同,因为它们提供了一种机制,可以在发出值时持续监听它们。

要测试返回流的方法,我们可以测试该方法是否被调用或检查值是否以正确的顺序发出。

class PostViewModel extends ChangeNotifier {
  ...
  PostRepository postRepository;
  final likesStreamController = StreamController<int>();
​
  PostViewModel(this.postRepository);
​
  ...
  void listenForLikes(int postId) {
    postRepository.listenForLikes(postId).listen((likes) {
      likesStreamController.add(likes);
    });
  }
}
​
​
@GenerateMocks([PostRepository])
void main() {
  MockPostRepository mockPostRepository = MockPostRepository();
  late PostViewModel postViewModel;
​
  setUp(() {
    postViewModel = PostViewModel(mockPostRepository);
  });
​
  test('Listen for likes works correctly', () {
    final mocklikesStreamController = StreamController<int>();
​
    when(mockPostRepository.listenForLikes(any))
        .thenAnswer((inv) => mocklikesStreamController.stream);
​
    postViewModel.listenForLikes(1);
​
    mocklikesStreamController.add(3);
    mocklikesStreamController.add(5);
    mocklikesStreamController.add(9);
​
    // checks if listen for likes is called
    verify(mockPostRepository.listenForLikes(1));
    expect(postViewModel.likesStreamController.stream, emitsInOrder([3, 5, 9]));
  });
}

在上面的示例中,我们添加了一个listenforLikes方法,该方法调用该PostRepository方法并返回一个我们可以监听的流。接下来,我们创建了一个测试来侦听流并检查方法是否以正确的顺序被调用和发出。

对于一些复杂的情况,我们可以使用expectLaterorexpectAsync1来代替只使用expect函数。

结论

就像大多数逻辑看起来一样简单,编写测试非常重要,因此我们不会重复 QA 这些功能。编写测试的目的之一是在您的应用程序变大时减少重复的 QA。

在本文中,我们了解了如何在编写单元测试时有效地使用 Mockito 生成模拟。我们还学习了如何使用 fakes 和参数匹配器来编写功能测试。文章来源地址https://www.toymoban.com/news/detail-474662.html

到了这里,关于使用 Mockito 对 Flutter 代码进行单元测试的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

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

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

相关文章

  • 搭建大型分布式服务(四十六)利用mockito不启动SpringBoot项目下进行单元测试

    SpringBoot支持集成Mockito做单元测试,有时候SpringBoot有很多外部依赖,在本地很难启动或者启动时间很长,而我们只想对某个方法进行测试,需要怎样做呢? 一、本文要点 接前文,我们已经已介绍在两种mock模式下,怎样根据入参返回自定义mock结果,但都是通过启动SpringBoot项

    2024年02月06日
    浏览(44)
  • Mockito单元测试基本使用

    本文参考: 【码农教程】手把手教你Mockito的使用 - 掘金 (juejin.cn) java - doReturn().when()与when().thenReturn() - 成长之路 - SegmentFault 思否 单元测试实践篇:Mock_阿里巴巴淘系技术团队官网博客的博客-CSDN博客 阿里是如何进行单元测试培训的?_Hollis Chuang的博客-CSDN博客 【Mockito】Mock

    2024年02月10日
    浏览(48)
  • 【单元测试】Mockito使用详解

    一个单元测试应该有如下特点: 应该是自动化的 应该可以快速运行 每个单元测试不应该依赖其它测试的结果和执行顺序,单元测试框架可以按任意的顺序执行每个测试 每个单元测试不应该依赖数据库,外部文件,或者任何长时间运行的任务。单元测试应该是独立的,不应该

    2024年04月15日
    浏览(84)
  • 使用Mockito针对多线程场景编写单元测试

    Mockito是一个Java的Mocking框架,主要用于编写单元测试。针对多线程编程的单元测试,可以使用Mockito的一些特性和技巧来完成。  Mockito支持异步调用技术,可以使用Mockito.when().thenReturnAsync()方法来模拟异步调用的返回值。这样可以模拟多线程编程的情况。 例如,假设有一个异

    2024年02月09日
    浏览(44)
  • SpringBoot单元测试--Mockito+Junit5框架使用

    作为程序员为了提前发现代码bug,优化代码; 通常我们写完某个功能模块代码后都需要写单元测试对代码块进行测试(特别是敏捷开发中);Java项目最常用的单元测试框架即为Junit(目前最新版本为Junit5),SpringBoot本身也整合了该框架。在写单元测试时代码块中的调到第三方接口方

    2024年02月02日
    浏览(45)
  • 单元测试利器——手把手教你使用Mockito

    作者:京东零售 秦浩然 从你成为开发人员的那一天起,写单元测试终究是你逃不开的宿命!那开发人员为什么不喜欢写单元测试呢?究其原因,无外乎是依赖。依赖其他的服务、依赖运行的环境、等等,各种依赖都成为了我们写单元测试的绊脚石。那现在有个单元测试利器

    2024年02月08日
    浏览(64)
  • 使用Glib中测试框架对C代码进行单元测试

    C++项目的测试框架比较常见的是Google的 gtest (前文CMake项目使用ctest+gtest进行单元测试有使用实例介绍 gtest ,感兴趣的读者可以去看看),也有一些其它框架,比如Boost中的测试框架。这些框架虽然也可以测试C代码,但是如果在一个纯C项目中引入这些的框架,则需要使用C+

    2023年04月22日
    浏览(36)
  • spring boot 单元测试JUnit5使用Mockito模拟Mock数据调用

    spring boot 单元测试JUnit5使用Mockito模拟Mock数据调用 好大一批新用法,大家静下心来好好看看吧 1. spring boot 使用 Mockito.when().thenReturn()模拟返回值 Mockito 是一种 Java mock 框架,他主要就是用来做 mock 测试的,他可以模拟任何 Spring 管理的 bean、模拟方法的返回值、模拟抛出异常…

    2024年02月15日
    浏览(55)
  • 【Java应用】使用Mockito进行模拟和测试桩

    Mockito是一个流行的Java模拟框架,用于编写单元测试代码时模拟(mock)和测试桩(stub)对象的行为。可轻松模拟Java类和接口的行为,帮助测试人员和开发人员更好地设计和执行单元测试。 使用Mockito,开发人员可以模拟一个对象,使其表现出某些预期的行为,而无需使用真实

    2024年02月11日
    浏览(36)
  • Mockito单元测试异常情况

    一、空指针异常 例子: 使用Collectors.groupingBy()进行分组时, 分组值存在null值 。 解决办法:分组值为null时,默认值为空字符,或者让数据不为空。 二、多个参数匹配异常 例如: 如果a和b一个是正常传参(list、map、string…)另一个是任意万能传参(any()、anyList()、anyString(

    2024年02月07日
    浏览(49)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包