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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 09:17:59