Flutter for OpenHarmony:mockito 单元测试的替身演员,轻松模拟复杂依赖(测试驱动开发必备) 深度解析与鸿蒙适配指南

Flutter for OpenHarmony:mockito 单元测试的替身演员,轻松模拟复杂依赖(测试驱动开发必备) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net

在这里插入图片描述

前言

在软件开发中,单元测试(Unit Testing)是保证代码质量的基石。然而,在测试某个具体的业务逻辑(如 UserService)时,我们往往会遇到各种外部依赖,比如数据库查询、网络请求、设备传感器等。

如果直接调用真实的 DatabaseHttpClient,不仅测试速度慢,而且容易因为网络抖动或环境问题导致测试失败。此外,我们很难复现一些极端场景(如 500 服务器错误、数据库连接超时)。

Mockito 就是为了解决这个问题而生的。它允许我们创建对象的 Mock(替身),并精确控制这些替身的行为(Stubbing)和验证它们的交互(Verification)。

在 OpenHarmony 应用开发中,使用 mockito 可以让我们在开发机(Host)上就能快速验证大部分业务逻辑,通过后再部署到鸿蒙真机进行集成测试,极大地提高了开发效率。

一、核心概念与工作原理

1.1 什么是 Mock?

Mock 对象是真实对象的“傀儡”。它继承自真实类,但不会执行真实类的任何代码。相反,它记录了所有对它的调用,并根据你的配置返回预设的值。

1.2 Mockito 的黑魔法:代码生成 (Codegen)

早期版本的 Mockito 依赖 noSuchMethod 和反射(Mirrors),但在 Flutter 中禁止使用反射(为了 AOT 优化)。因此,现在的 mockito 配合 build_runner 使用,在编译时生成 Mock 类。

@GenerateMocks

生成

使用

验证

RealClass.dart

build_runner

RealClass.mocks.dart

单元测试

Mock 对象

1.3 核心操作三部曲

  1. Stubbing (桩): when(mock.method()).thenReturn(value) —— “当调用这个方法时,请返回这个值”。
  2. Execution (执行): 运行被测代码,被测代码会调用 Mock 对象。
  3. Verification (验证): verify(mock.method()) —— “验证这个方法是否被调用过,且参数正确”。

二、集成与基础用法

2.1 添加依赖

mockito 通常只在 dev_dependencies 中使用。

dev_dependencies:flutter_test:sdk: flutter mockito: ^5.6.3 build_runner: ^2.4.0 

2.2 定义被测类

假设我们有一个从 API 获取用户信息的服务:

// user_api.dartimport'dart:convert';import'package:http/http.dart'as http;classUserApi{finalhttp.Client client;UserApi(this.client);Future<String>fetchUserName(int id)async{final response =await client.get(Uri.parse('https://api.example.com/users/$id'));if(response.statusCode ==200){returnjsonDecode(response.body)['name'];}else{throwException('Failed to load user');}}}

2.3 生成 Mock 类

创建一个测试文件,使用注解 @GenerateMocks

// user_api_test.dartimport'package:mockito/annotations.dart';import'package:http/http.dart'as http;import'package:flutter_test/flutter_test.dart';import'package:mockito/mockito.dart';import'user_api.dart';// 引入生成的文件import'user_api_test.mocks.dart';@GenerateMocks([http.Client])voidmain(){// ...}

然后在终端运行命令生成代码:

flutter pub run build_runner build 

2.4 编写测试用例

voidmain(){ late MockClient mockClient; late UserApi userApi;setUp((){ mockClient =MockClient(); userApi =UserApi(mockClient);});test('returns name if the http call completes successfully',()async{// 1. Stubbing: 模拟 http.Client 返回 200 OKwhen(mockClient.get(Uri.parse('https://api.example.com/users/1'))).thenAnswer((_)async=>http.Response('{"name": "张三"}',200));// 2. Executionfinal name =await userApi.fetchUserName(1);// 3. Verification: 验证结果expect(name,'张三');// 验证 get 方法确实被调用了一次verify(mockClient.get(Uri.parse('https://api.example.com/users/1'))).called(1);});test('throws an exception if the http call completes with an error',(){// 1. Stubbing: 模拟返回 404when(mockClient.get(Uri.parse('https://api.example.com/users/1'))).thenAnswer((_)async=>http.Response('Not Found',404));// 2. Execution & Verificationexpect(userApi.fetchUserName(1), throwsException);});}
在这里插入图片描述


在这里插入图片描述

三、进阶技巧

3.1 参数匹配 (Argument Matchers)

有时候我们不关心具体的参数值,只关心参数类型。

// 匹配任何 Uriwhen(mockClient.get(any)).thenAnswer(...)// 命名参数匹配when(obj.method(foo:anyNamed('foo'))).thenReturn(...)// 捕获参数进行验证test('captures arguments',(){// ... trigger ...// 验证调用并捕获参数final captured =verify(mockClient.get(captureAny)).captured;print(captured.first);// 输出 Uri 对象});

3.2 顺序验证 (InOrder)

验证多个方法的调用顺序。

test('verify interaction order',(){final cat =MockCat(); cat.eat(); cat.sleep();verifyInOrder([ cat.eat(), cat.sleep(),]);});

3.3 模拟异常与重试逻辑

开发鸿蒙应用时,网络波动是常态。我们可以 Mock 抛出异常来测试 App 的重试机制是否工作。

test('测试重试逻辑',()async{// 第一次调用抛出异常,第二次成功when(mockApi.getData()).thenThrow(Exception('Network Error')).thenAnswer((_)async=>'Success');// 调用业务逻辑...});
在这里插入图片描述

四、OpenHarmony 适配与实战:模拟鸿蒙原生通道 (Platform Channel)

在 OpenHarmony 开发中,我们经常需要通过 MethodChannel 调用系统的原生能力(如获取电池电量、调用 HiLog)。在写 UI 测试或逻辑测试时,我们不能真的去调底层,因为测试环境可能只是一个 Dart VM。

我们可以 Mock MethodChannel 的 handlers。

4.1 场景:电池电量获取

假设我们有一个 BatteryService

import'package:flutter/services.dart';classBatteryService{staticconst platform =MethodChannel('ohos.samples.battery');Future<String>getBatteryLevel()async{try{final int result =await platform.invokeMethod('getBatteryLevel');return'当前电量 $result%';}onPlatformExceptioncatch(e){return"获取电量失败: '${e.message}'.";}}}

4.2 编写测试

Flutter 官方提供了 TestDefaultBinaryMessenger 来模拟底层消息。但在单元测试层面,mockito 也可以派上用场,或者我们可以直接 mock 对 BatteryService 的依赖。

这里我们展示如何使用 mockito 测试一个依赖 BatteryServiceViewModel

// battery_view_model.dartclassBatteryViewModel{finalBatteryService _service;String status ='未知';BatteryViewModel(this._service);Future<void>refresh()async{ status =await _service.getBatteryLevel();}}// battery_view_model_test.dart@GenerateMocks([BatteryService])import'battery_view_model_test.mocks.dart';voidmain(){test('ViewModel updates status from service',()async{final mockService =MockBatteryService();final viewModel =BatteryViewModel(mockService);// Stubbing: 模拟鸿蒙设备返回了 100% 电量when(mockService.getBatteryLevel()).thenAnswer((_)async=>'当前电量 100%');await viewModel.refresh();expect(viewModel.status,'当前电量 100%');});}

4.3 为什么这对鸿蒙开发重要?

鸿蒙设备的 API(如 @ohos.batteryInfo)只能在真机或模拟器运行。如果你的 Dart 代码中混杂了原生调用,一旦离开真机环境就会报错。

通过 Dependency Injection (依赖注入) 配合 Mockito,你可以让 Dart 业务逻辑与鸿蒙原生 API 解耦。

  • 开发时:在 PC 上运行测试,Mock 掉鸿蒙 API,开发效率极高。
  • 运行时:注入真实的 BatteryService,调用鸿蒙底层。

五、总结

mockito 是测试驱动开发(TDD)的利器。它让我们能够隔离关注点,专注于当前模块的逻辑,而不必担心外部世界的复杂性。

对于 OpenHarmony 开发者,掌握 Mock 技术意味着你可以构建出架构更清晰、可测试性更强的应用。无论是模拟网络、数据库,还是模拟鸿蒙特有的系统服务,mockito 都能帮你轻松搞定。

最佳实践

  1. 多用接口:针对 Interface 编程,而不是针对 Implementation。Mock 接口比 Mock 具体类更容易且风险更小。
  2. 不要 Mock 数据类:对于 DTO(Data Transfer Object)或简单的实体类,直接 new 一个真的对象,不要 Mock 它。
  3. 保持测试简单:如果你的 Mock 配置(when…then…)写了几十行,说明被测代码可能耦合太重,需要重构了。

六、完整实战示例:带超时的网络请求模拟器

import'package:mockito/mockito.dart';import'package:test/test.dart';// 1. 业务接口abstractclassNetworkService{Future<String>request(String path);}// 2. 模拟生成的 Mock 类classMockNetworkServiceextendsMockimplementsNetworkService{@overrideFuture<String>request(String? path)=>super.noSuchMethod(Invocation.method(#request,[path]), returnValue:Future.value(""),)asFuture<String>;}// 3. 业务逻辑类classDataManager{finalNetworkService _service;DataManager(this._service);Future<String>safeRequest()async{try{// 模拟 2 秒延迟外的逻辑returnawait _service.request('/data').timeout(Duration(seconds:1));}catch(e){return"Timeout or Error";}}}voidmain(){test('模拟请求超时场景',()async{final mockService =MockNetworkService();final manager =DataManager(mockService);// Stubbing: 让请求延迟 2 秒返回,从而触发业务层的超时when(mockService.request(any)).thenAnswer((_)async{awaitFuture.delayed(Duration(seconds:2));return"Delayed Data";});final result =await manager.safeRequest();expect(result,"Timeout or Error");verify(mockService.request('/data')).called(1);});}
在这里插入图片描述

Read more

c++之inline关键字从基础到通天(一篇即毕业系列)

c++之inline关键字从基础到通天(一篇即毕业系列)

文章目录 * inline 是一个请求(而非命令) * inline 函数通常用于小函数 * inline 函数的定义通常放在头文件中 * inline 函数不能包含复杂的控制结构 * 编译器可能忽略 inline 请求 * 验证是否 inline * 代码块 * 汇编代码分析 * 其他验证方法 * 推荐阅读 欢迎收看一篇即毕业系列,本系列的其它内容如同本篇一样优秀喔!! 那么话不多说! 关于 inline,我们直接了解以下几个知识点即可。 inline 是一个请求(而非命令) inline 关键字用于向编译器发出一个请求,建议将函数体在每个调用点内联展开。这意味着编译器在编译过程中,可能会将函数的代码直接插入到调用该函数的地方,而不是通过通常的函数调用机制来执行。 需要注意的是,inline 只是一个建议,编译器可以选择是否接受这个建议。编译器可能会基于多种因素(如函数的大小、复杂性、调用频率以及整体代码的优化目标)来决定是否进行内联展开。 inline 函数通常用于小函数 inline 函数通常用于那些执行速度快且调用频繁的小

By Ne0inhk
从二叉树到 STL:揭开 set 容器的本质与用法

从二叉树到 STL:揭开 set 容器的本质与用法

前言:         上次介绍完二叉搜索树后,更新中断了一段时间,先向大家致歉。最近学习状态有些起伏,但我正在努力调整,相信很快会恢复节奏。今天我们继续深入探讨——关联容器,它在算法和工程中都非常常见和重要。 1.之前的遗漏         我之前写的二叉搜索树其实没有写完,我仅仅写了一个节点只能存储一个值的二叉搜索树。在我们日常的工作中,这种树的使用率其实还是比较低的。最受欢迎的是里面存储两个值的二叉搜索树,这个可以类比Python中的字典。相信学过python的读者对此不陌生,字典其实存放了一对值,分别是Key和Value,类比英文字典中的英语和汉字,我们通过英文(Key)来找到对应的中文(Value)。这其实才是我们常使用到的二叉搜索树,下面我通过举例来帮助各位理解这两棵树的区别。 1.1.Key搜索场景         举个例子,现在很多小区配有地下停车库。业主的车牌号会录入系统中,车辆进入时由系统识别并判断是否允许进入。这就是典型的 Key 搜索场景 —— 只需根据一个关键字(车牌号)进行查找。 1.2.Key/Value搜索场景         还是以我们

By Ne0inhk
C++ 模板进阶:特化、萃取与可变参数模板

C++ 模板进阶:特化、萃取与可变参数模板

C++ 模板进阶:特化、萃取与可变参数模板 💡 学习目标:掌握模板进阶技术的核心用法,理解模板特化的深层应用、类型萃取的实现原理,以及可变参数模板的灵活使用,提升泛型编程的实战能力。 💡 学习重点:模板特化的进阶场景、类型萃取工具的设计与应用、可变参数模板的展开技巧、折叠表达式的使用方法。 一、模板特化进阶:处理复杂类型场景 💡 模板特化不只是针对单一类型的定制,还能处理指针、引用、数组等复杂类型,实现更精细的类型适配逻辑。 1.1 指针类型的模板特化 通用模板默认处理普通类型,我们可以为指针类型单独编写特化版本,实现指针专属的逻辑。 #include<iostream>#include<string>usingnamespace std;// 通用模板:处理普通类型template<typenameT>classTypeProcessor{public:staticvoidprocess(T data){ cout

By Ne0inhk

为什么要用线程池(C++)?

1)不用线程池时: void handle_request() {     std::thread t([]{         // 干活     });     t.detach(); } 问题很明显: * ❌ 频繁 new thread / destroy thread,开销大 * ❌ 线程数量不可控,可能把系统拖死 * ❌ 不好统一管理、退出、回收 线程池的本质一句话: 线程提前创建好,反复用;任务丢进去,线程自己取。 二、线程池的核心模型(一定要理解) 线程池 ≈ 三样东西 +--------------------+ |   Task Queue       |  <- function<void()> +--------------------+         ▲          | +--------------------+ |   Worker Threads   |  多个 while(true) +--------------------+

By Ne0inhk