如何在Flutter中对CachedNetworkImage进行Widget测试?
测试CachedNetworkImage的正确方法
我明白你遇到的问题——CachedNetworkImage因为自带缓存机制(默认依赖sqflite),所以单纯用HttpOverrides模拟网络请求还不够,得同时处理缓存层的Mock,而且要确保缓存初始化时需要的sqflite方法都被正确模拟。下面是一步步的解决方案:
1. 完善Sqflite的MethodChannel Mock
CachedNetworkImage的缓存初始化和操作需要调用sqflite的多个方法,不只是getDatabasesPath。你需要模拟openDatabase、query、insert、update等核心方法,避免缓存初始化失败导致网络请求根本没触发。
2. 确保HttpOverrides正确拦截网络请求
CachedNetworkImage内部会使用自己的HttpClient实例,所以要确保HttpOverrides.runZoned能覆盖到它,或者直接给CachedNetworkImage传入你Mock的HttpClient。
3. 完整测试代码示例
import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:sqflite/sqflite.dart'; const List<int> kTransparentImage = <int>[ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, ]; void main() async { TestWidgetsFlutterBinding.ensureInitialized(); await setupSqfliteMock(); testWidgets('CachedNetworkImage should load mocked image', (WidgetTester tester) async { final MockHttpClient client = MockHttpClient(); final MockHttpClientRequest request = MockHttpClientRequest(); final MockHttpClientResponse response = MockHttpClientResponse(); final MockHttpHeaders headers = MockHttpHeaders(); await HttpOverrides.runZoned(() async { await tester.pumpWidget( MaterialApp( home: CachedNetworkImage( imageUrl: 'https://www.example.com/images/frame.png', errorWidget: (context, err, o) => const Text('Error'), ), ), ); // 等待框架处理网络请求和缓存逻辑 await tester.pumpAndSettle(); // 验证网络请求是否被触发 verify(client.getUrl(Uri.parse('https://www.example.com/images/frame.png'))).called(1); }, createHttpClient: (_) { when(client.getUrl(any)).thenAnswer((_) async { when(request.headers).thenReturn(headers); when(request.close()).thenAnswer((_) async { when(response.contentLength).thenReturn(kTransparentImage.length); when(response.statusCode).thenReturn(HttpStatus.ok); when(response.listen(any)).thenAnswer((Invocation invocation) { final void Function(List<int>) onData = invocation.positionalArguments[0] as void Function(List<int>); return Stream.value(Uint8List.fromList(kTransparentImage)).listen(onData); }); return response; }); return request; }); return client; }); }, skip: isBrowser); } // Mock类定义 class MockHttpClient extends Mock implements HttpClient {} class MockHttpClientRequest extends Mock implements HttpClientRequest {} class MockHttpClientResponse extends Mock implements HttpClientResponse {} class MockHttpHeaders extends Mock implements HttpHeaders {} // 初始化Sqflite的Mock Future<void> setupSqfliteMock() async { const MethodChannel channel = MethodChannel('com.tekartik.sqflite'); final Directory tempDir = await Directory.systemTemp.createTemp(); channel.setMockMethodCallHandler((MethodCall methodCall) async { switch (methodCall.method) { case 'getDatabasesPath': return tempDir.path; case 'openDatabase': // 返回一个模拟的数据库句柄(这里用任意整数即可) return 1; case 'query': // 模拟缓存查询,返回空列表表示没有缓存 return <Map<String, dynamic>>[]; case 'insert': // 模拟缓存插入操作 return 1; case 'update': // 模拟缓存更新操作 return 1; case 'close': return null; default: throw UnimplementedError('Method ${methodCall.method} not mocked'); } }); }
关键说明:
- Sqflite Mock完善:新增了
openDatabase、insert、update等方法的模拟,确保缓存初始化和读写操作不会抛出错误,这样CachedNetworkImage才会正常发起网络请求。 - 使用
pumpAndSettle:CachedNetworkImage的加载是异步的,包括缓存检查和网络请求,所以需要调用tester.pumpAndSettle()等待所有异步操作完成。 - 包裹在MaterialApp中:CachedNetworkImage需要
MediaQuery等上下文依赖,所以要放在MaterialApp里测试。 - 验证网络请求:用
verify来确认mock的HttpClient确实被调用了,确保测试逻辑生效。
另外,如果你不想处理缓存的Mock,也可以尝试使用CachedNetworkImageProvider并传入cacheManager: MockCacheManager()(需要自己实现MockCacheManager),完全绕过真实的缓存操作,专注测试网络请求部分。
内容的提问来源于stack exchange,提问作者Mousa Najafi
相关产品推荐
相关产品推荐

