Flutter Drift(Moor)单元测试报错如何修复?
Flutter Drift 本地存储单元测试失败修复方案
问题根因
- 直接用
Mock实现AppDatabase时,没有手动mock Drift自动生成的homeDaogetter、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逻辑,测试结果和线上运行行为完全一致,同时运行速度远快于磁盘数据库。
- 首先改造
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, ); }
- 编写基于内存数据库的测试用例,无需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
相关产品推荐
相关产品推荐

