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

Flutter Drift(Moor)单元测试报错如何修复?

Flutter Drift 本地存储单元测试失败修复方案

问题根因

  • 直接用Mock实现AppDatabase时,没有手动mock Drift自动生成的homeDao getter、close()方法的返回值,Mock类默认返回null,触发type 'Null' is not a subtype of type 'Future<void>'空类型错误。
  • 原有stub写法逻辑错误:链式调用databaseHandler.homeDao.getRepositories()打桩时,首先会读取databaseHandler.homeDao属性,该属性未设置返回值默认返回null,后续给这个位置塞的Future<List<RepositoriesTableData>>返回值和期望的HomeDao类型不匹配,触发type 'Future<List<RepositoriesTableData>>' is not a subtype of type 'HomeDao'错误。
  • 直接Mock整个数据库类测试Drift逻辑本身不是最优方案,Drift官方推荐使用内存数据库做真实逻辑测试,能避免Mock行为和真实数据库逻辑不一致的问题。

修复方案

方案1:修正现有Mock写法(仅用于验证上层数据源调用逻辑)

不要直接链式对Mock的AppDatabase打桩,单独Mock HomeDao实例,手动给AppDatabase的homeDao getter、close()方法设置正确返回值,修正后测试代码如下:

// 单独Mock Dao层,避免链式调用打桩的类型问题
class MockHomeDao extends Mock implements HomeDao {}
class MockAppDatabase extends Mock implements AppDatabase {}

void main() {
  late GTHomeLocalDataSource localDataSource;
  late MockAppDatabase mockDatabase;
  late MockHomeDao mockHomeDao;

  setUp(() {
    mockDatabase = MockAppDatabase();
    mockHomeDao = MockHomeDao();
    // 手动设置homeDao getter返回mock的Dao实例
    when(() => mockDatabase.homeDao).thenReturn(mockHomeDao);
    // 手动设置close方法的异步返回值,避免返回null
    when(() => mockDatabase.close()).thenAnswer((_) async => {});

    localDataSource = GTHomeLocalDataSourceImpl(
      appDatabase: mockDatabase,
    );
  });

  group("GTHomeLocalDataSource -", () {
    test('调用getRepositories时正确透传Dao层返回结果', () async {
      // 准备测试假数据
      final testData = repositoriesDummyTable;
      // 对Dao层方法打桩
      when(() => mockHomeDao.getRepositories())
          .thenAnswer((_) async => testData);

      // 执行目标方法
      final result = await localDataSource.getRepositories();

      // 校验调用逻辑和返回结果
      verify(() => mockHomeDao.getRepositories()).called(1);
      expect(result, isA<List<RepositoriesTableData>>());
      expect(result.length, repositoriesDummyTable.length);
      expect(result.first.language, repositoriesDummyTable.first.language);
    });
  });

  tearDown(() async {
    await mockDatabase.close();
  });
}

注意:该方案仅能验证数据源层是否正确调用数据库方法,无法验证表结构定义、SQL查询逻辑的正确性,所有数据库行为都是手动Mock的,可能和真实运行逻辑存在偏差。


方案2:Drift官方推荐测试写法(推荐,覆盖真实数据库逻辑)

不需要Mock任何数据库类,使用Drift自带的内存数据库运行真实SQL逻辑,测试结果和线上运行行为完全一致,同时运行速度远快于磁盘数据库。

  1. 首先改造AppDatabase构造方法,支持外部传入QueryExecutor,兼容测试和生产环境:
@DriftDatabase(
  tables: [RepositoriesTable],
  daos: [HomeDao],
)
class AppDatabase extends _$AppDatabase {
  // 测试用构造,支持传入自定义执行器
  AppDatabase(QueryExecutor executor) : super(executor);

  // 生产环境用工厂构造,保留原有sqflite打开逻辑
  factory AppDatabase.production() => AppDatabase(_openConnection());

  @override
  int get schemaVersion => 1;
}

QueryExecutor _openConnection() {
  return SqfliteQueryExecutor.inDatabaseFolder(
    path: 'db.sqlite',
    logStatements: true,
  );
}
  1. 编写基于内存数据库的测试用例,无需Mock:
import 'package:drift/native.dart';

void main() {
  late AppDatabase testDatabase;
  late GTHomeLocalDataSource localDataSource;

  setUp(() {
    // 每个测试用例初始化全新的内存数据库,用例之间数据完全隔离
    testDatabase = AppDatabase(NativeDatabase.memory());
    localDataSource = GTHomeLocalDataSourceImpl(appDatabase: testDatabase);
  });

  group("GTHomeLocalDataSource 真实逻辑测试 -", () {
    test('插入数据后调用getRepositories可正确返回存储的数据', () async {
      // 准备测试数据并插入测试库
      final testRepo = repositoriesDummyTable.first;
      await testDatabase.into(testDatabase.repositoriesTable).insert(testRepo);

      // 执行查询方法
      final result = await localDataSource.getRepositories();

      // 校验返回结果和插入数据一致
      expect(result.length, 1);
      expect(result.first.language, testRepo.language);
      expect(result.first.id, testRepo.id);
    });
  });

  tearDown(() async {
    // 关闭测试数据库释放资源
    await testDatabase.close();
  });
}

该方案优势:

  • 不需要编写大量Mock打桩代码,不会出现类型不匹配、空返回的低级错误
  • 真实运行SQL逻辑,可覆盖表结构、查询语句、Dao层逻辑的正确性,测试可信度远高于Mock方案
  • 内存数据库无磁盘IO,测试运行速度极快

内容的提问来源于stack exchange,提问作者R Rifa Fauzi Komara

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 05:27:45