Dart/Flutter中Isolate内部方法调用的Mock验证问题
Flutter Isolate 中 Mock 无法捕获方法调用的问题解决
问题描述
场景
我有一个Repository类,负责从数据库获取原始数据并进行映射。由于数据量较大,尽管映射是同步操作,但按照官方建议将其放在Isolate中执行(注:我使用Isolate.run,效果与compute一致)。
问题
我使用Mocktail包进行测试(Mockito也存在同样问题),Isolate内部的映射函数明明被调用了,但无法被Mockito的verify方法捕获。经过调研,我认为原因是Isolate不会使用传入的mapper原对象,而是会复制该对象并调用副本的方法。
疑问
- 我的假设是否正确——
verify无法捕获调用是因为Isolate复制了mapper对象? - 应如何修改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
相关产品推荐
相关产品推荐

