You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Dart/Flutter中Isolate内部方法调用的Mock验证问题

Flutter Isolate 中 Mock 无法捕获方法调用的问题解决

问题描述

场景

我有一个Repository类,负责从数据库获取原始数据并进行映射。由于数据量较大,尽管映射是同步操作,但按照官方建议将其放在Isolate中执行(注:我使用Isolate.run,效果与compute一致)。

问题

我使用Mocktail包进行测试(Mockito也存在同样问题),Isolate内部的映射函数明明被调用了,但无法被Mockito的verify方法捕获。经过调研,我认为原因是Isolate不会使用传入的mapper原对象,而是会复制该对象并调用副本的方法。

疑问

  1. 我的假设是否正确——verify无法捕获调用是因为Isolate复制了mapper对象?
  2. 应如何修改Repository代码或测试代码,以验证映射方法被调用?

Repository 代码

class Repository{
final Source source;
final Mapper mapper;
Repository({required this.source, required this.mapper});

Future<List<MappedData>> getMappedData() async {
 final rawData = source.getRawData();
 final mappedData = await Isolate.run(()=>
 _mapRawData(rawData, mapper);
 );
 return mappedData;
 }


 static List<MappedData> _mapRawData(List<RawData> rawData, Mapper mapper){
 return rawData.map((e)=> mapper(e)).toList();
 }

}

测试代码

import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class MockSource extends Mock implements Source{}
class MockMapper extends Mock implements Mapper{}

void main() {
 late MockSource source;
 late MockMapper mapper;
 late Repository repository;

 final rawData = [
 RawData(1),
 RawData(2),
 ];

 final mappedData = [
 MappedData(1),
 MappedData(2),
 ];

 setUp(){
 source= MockSource();
 mapper = MockMapper();
 repository = Repository (source:source, mapper: mapper);
 }

 test(
      'Repository getMappedData should call source and mapper',
      () async {
  //assign
  when(()=> source.getRawData()).thenReturn(rawData);
  when(()=> mapper(rawData[0])).thenReturn(mappedData[0]);
  when(()=> mapper(rawData[1])).thenReturn(mappedData[1]);
  //act
  final result = await repository.getMappedData();
  //assert
  expect(result, mappedData);
  verify(()=> source.getRawData()).called(1);
  verify(()=> mapper(rawData[0])).called(1);
  verify(()=> mapper(rawData[1])).called(1);
}

注:expect断言正常通过,但verify方法触发错误,提示从未调用过目标方法。

解答

1. 关于假设的正确性

你的假设完全正确。Isolate在Dart中是独立的执行线程,所有传入Isolate的对象都会通过序列化/反序列化生成全新的副本。测试中创建的MockMapper实例会被复制到Isolate内部,实际调用的是副本的方法,而非你在测试代码中持有的原对象。因此verify无法捕获这些调用——它只能监控原mock对象的方法调用。

2. 解决方案

有两种可行的修改方向,根据你的测试需求选择:

方案一:重构Repository,将映射逻辑与Isolate解耦(推荐)

把映射逻辑封装成独立的纯函数,而非依赖Mapper对象。这样在测试时可以通过计数或mock函数来验证调用:

修改后的Repository代码:

class Repository{
final Source source;
final Mapper mapper;
Repository({required this.source, required this.mapper});

Future<List<MappedData>> getMappedData() async {
 final rawData = source.getRawData();
 // 将mapper的调用逻辑包装成可序列化的函数
 final mappedData = await Isolate.run(()=>
 _mapRawData(rawData, (RawData e) => mapper(e));
 );
 return mappedData;
 }

 // 接收纯函数作为映射逻辑参数
 static List<MappedData> _mapRawData(List<RawData> rawData, MappedData Function(RawData) mapFunction){
 return rawData.map(mapFunction).toList();
 }
}

对应的测试代码:

test(
 'Repository getMappedData should call source and mapper',
 () async {
  //assign
  when(()=> source.getRawData()).thenReturn(rawData);
  // 记录mapper的调用次数
  var callCount = 0;
  when(()=> mapper(any())).thenAnswer((invocation) {
    callCount++;
    return mappedData[callCount-1];
  });
  //act
  final result = await repository.getMappedData();
  //assert
  expect(result, mappedData);
  verify(()=> source.getRawData()).called(1);
  expect(callCount, 2); // 验证mapper被调用两次
}

方案二:在测试中跳过Isolate执行

通过依赖注入增加开关,让Repository在测试环境下直接执行映射逻辑,不使用Isolate:

修改Repository代码:

class Repository{
final Source source;
final Mapper mapper;
final bool useIsolate;

// 默认启用Isolate,测试时可关闭
Repository({required this.source, required this.mapper, this.useIsolate = true});

Future<List<MappedData>> getMappedData() async {
 final rawData = source.getRawData();
 if(useIsolate){
   return await Isolate.run(()=> _mapRawData(rawData, mapper));
 } else {
   return _mapRawData(rawData, mapper);
 }
 }

 static List<MappedData> _mapRawData(List<RawData> rawData, Mapper mapper){
 return rawData.map((e)=> mapper(e)).toList();
 }
}

测试时禁用Isolate:

setUp(){
 source= MockSource();
 mapper = MockMapper();
 // 测试环境下关闭Isolate
 repository = Repository(source:source, mapper: mapper, useIsolate: false);
 }

test(
 'Repository getMappedData should call source and mapper',
 () async {
  //assign
  when(()=> source.getRawData()).thenReturn(rawData);
  when(()=> mapper(rawData[0])).thenReturn(mappedData[0]);
  when(()=> mapper(rawData[1])).thenReturn(mappedData[1]);
  //act
  final result = await repository.getMappedData();
  //assert
  expect(result, mappedData);
  verify(()=> source.getRawData()).called(1);
  verify(()=> mapper(rawData[0])).called(1);
  verify(()=> mapper(rawData[1])).called(1);
}

这个方案无需修改核心业务逻辑,仅通过参数控制执行路径,适合快速适配测试。

内容的提问来源于stack exchange,提问作者salim.elkh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 12:34:51