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

Flutter测试中使用Mockito模拟Firestore出现类型不匹配错误

Flutter测试中Mockito模拟Firestore的泛型类型不匹配问题

错误信息

The argument type 'MockCollectionReference<Object?>' can't be assigned to the parameter type 'CollectionReference<Map<String, dynamic>>'.

问题背景

项目使用Firestore存储数据,尝试用Mockito模拟FirebaseFirestore、CollectionReference、DocumentReference等类进行单元测试,在设置when(...).thenReturn(...)存根方法时,出现泛型类型不匹配的错误。

配置情况

  • 已通过@GenerateMocks注解为FirebaseAuth、FirebaseStorage、FirebaseMessaging、FirebaseFirestore、CollectionReference、DocumentReference等类生成Mock对象
  • 在测试的setUp方法中,使用这些Mock对象存根Firestore的collection、doc方法

测试代码示例

@GenerateMocks([
FirebaseAuth,
FirebaseStorage,
FirebaseMessaging,
FirebaseFirestore,
CollectionReference,
DocumentReference,
CheckPhoneAuthUserUseCase,
CheckAnonymousUserUsecase,
GetCurrentUidUseCase,
WriteCurrentTaskToLogUseCase,
])

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();
  setupFirebaseCoreMocks(); // Mocks initialization for Firebase

  late AdministrationRemoteDataSourceRepositoryImpl repository;
  late MockCheckPhoneAuthUserUseCase mockCheckPhoneAuthUserUseCase;
  late MockCheckAnonymousUserUsecase mockCheckAnonymousUserUseCase;
  late MockGetCurrentUidUseCase mockGetCurrentUidUseCase;
  late MockWriteCurrentTaskToLogUseCase mockWriteCurrentTaskToLogUseCase;
  late MockFirebaseFirestore mockFirebaseFirestore;
  late MockCollectionReference mockCollectionReference;
  late MockDocumentReference mockDocumentReference;
  late MockFirebaseMessaging mockFirebaseMessaging;
  late MockFirebaseStorage mockFirebaseStorage;
  late MockFirebaseAuth mockFirebaseAuth;

  setUpAll(() async {
    await Firebase.initializeApp(); // Initialize Firebase app
  });

  setUp(() {
    mockCheckPhoneAuthUserUseCase = MockCheckPhoneAuthUserUseCase();
    mockCheckAnonymousUserUseCase = MockCheckAnonymousUserUsecase();
    mockGetCurrentUidUseCase = MockGetCurrentUidUseCase();
    mockWriteCurrentTaskToLogUseCase = MockWriteCurrentTaskToLogUseCase();
    mockFirebaseFirestore = MockFirebaseFirestore();
    mockCollectionReference = MockCollectionReference();
    mockDocumentReference = MockDocumentReference();
    mockFirebaseAuth = MockFirebaseAuth();
    mockFirebaseStorage = MockFirebaseStorage();
    mockFirebaseMessaging = MockFirebaseMessaging();

    repository = AdministrationRemoteDataSourceRepositoryImpl(
      firebaseFirestore: mockFirebaseFirestore,
      firebaseMessaging: mockFirebaseMessaging,
      firebaseAuth: mockFirebaseAuth,
      firebaseStorage: mockFirebaseStorage,
      remoteService: null,
    );

    when(mockFirebaseFirestore.collection(any))
        .thenReturn(mockCollectionReference);
    when(mockCollectionReference.doc(any))
        .thenReturn(mockDocumentReference);
  });

  group('verifyStore', () {
    const storeId = 'testStoreId';
    const currentState = false;
    const uid = 'testUid';

    test('should return Success when all checks pass and store is verified',
        () async {
      // Arrange
      when(mockCheckPhoneAuthUserUseCase.call()).thenAnswer((_) async => true);
      when(mockCheckAnonymousUserUseCase.call()).thenAnswer((_) async => false);
      when(mockGetCurrentUidUseCase.call()).thenAnswer((_) async => uid);
      when(mockDocumentReference.update(any))
          .thenAnswer((_) async => Right(Success(message: "Done")));
      when(mockWriteCurrentTaskToLogUseCase.call(any))
          .thenAnswer((_) async => null);

      // Act
      final result = await repository.verifyStore(storeId, currentState);

      // Assert
      expect(result, Right(Success(message: "Successfully updated")));
      verify(mockCheckPhoneAuthUserUseCase.call());
      verify(mockCheckAnonymousUserUseCase.call());
      verify(mockGetCurrentUidUseCase.call());
      verify(mockDocumentReference.update(any));
      verify(mockWriteCurrentTaskToLogUseCase.call(any));
    });

    test('should return Failure when user is not authenticated with phone number',
        () async {
      // Arrange
      when(mockCheckPhoneAuthUserUseCase.call()).thenAnswer((_) async => false);

      // Act
      final result = await repository.verifyStore(storeId, currentState);

      // Assert
      expect(result, Left(Failure(message: "Invalid access")));
      verify(mockCheckPhoneAuthUserUseCase.call());
      verifyNever(mockCheckAnonymousUserUseCase.call());
      verifyNever(mockGetCurrentUidUseCase.call());
      verifyNever(mockDocumentReference.update(any));
    });

    test('should return Failure when user is anonymous', () async {
      // Arrange
      when(mockCheckPhoneAuthUserUseCase.call()).thenAnswer((_) async => true);
      when(mockCheckAnonymousUserUseCase.call()).thenAnswer((_) async => true);

      // Act
      final result = await repository.verifyStore(storeId, currentState);

      // Assert
      expect(result, Left(Failure(message: "Invalid access")));
      verify(mockCheckPhoneAuthUserUseCase.call());
      verify(mockCheckAnonymousUserUseCase.call());
      verifyNever(mockGetCurrentUidUseCase.call());
      verifyNever(mockDocumentReference.update(any));
    });

    test('should return Failure when there is a SocketException', () async {
      // Arrange
      when(mockCheckPhoneAuthUserUseCase.call()).thenAnswer((_) async => true);
      when(mockCheckAnonymousUserUseCase.call()).thenAnswer((_) async => false);
      when(mockGetCurrentUidUseCase.call()).thenAnswer((_) async => uid);
      when(mockDocumentReference.update(any)).thenThrow(SocketException('No Internet'));

      // Act
      final result = await repository.verifyStore(storeId, currentState);

      // Assert
      expect(result, Left(Failure(message: 'No Internet')));
      verify(mockCheckPhoneAuthUserUseCase.call());
      verify(mockCheckAnonymousUserUseCase.call());
      verify(mockGetCurrentUidUseCase.call());
      verifyNever(mockDocumentReference.update(any));
    });

    test('should return Failure when there is an unexpected exception', () async {
      // Arrange
      when(mockCheckPhoneAuthUserUseCase.call()).thenAnswer((_) async => true);
      when(mockCheckAnonymousUserUseCase.call()).thenAnswer((_) async => false);
      when(mockGetCurrentUidUseCase.call()).thenAnswer((_) async => uid);
      when(mockDocumentReference.update(any)).thenThrow(Exception('Unexpected error'));

      // Act
      final result = await repository.verifyStore(storeId, currentState);

      // Assert
      expect(result, Left(Failure(message: 'Exception: Unexpected error')));
      verify(mockCheckPhoneAuthUserUseCase.call());
      verify(mockCheckAnonymousUserUseCase.call());
      verify(mockGetCurrentUidUseCase.call());
      verifyNever(mockDocumentReference.update(any));
    });
  });
}

解决方法

1. 生成带指定泛型的Mock类

Mockito默认生成的MockCollectionReference和MockDocumentReference泛型为Object?,但实际代码中使用的是CollectionReference<Map<String, dynamic>>,需要在@GenerateMocks中通过customMocks指定泛型参数:

@GenerateMocks([
  FirebaseAuth,
  FirebaseStorage,
  FirebaseMessaging,
  FirebaseFirestore,
  CheckPhoneAuthUserUseCase,
  CheckAnonymousUserUsecase,
  GetCurrentUidUseCase,
  WriteCurrentTaskToLogUseCase,
], customMocks: [
  // 指定CollectionReference的泛型为Map<String, dynamic>
  MockSpec<CollectionReference<Map<String, dynamic>>>(),
  // 指定DocumentReference的泛型为Map<String, dynamic>
  MockSpec<DocumentReference<Map<String, dynamic>>>(),
])

2. 调整测试中的变量类型

将测试代码中的Mock变量类型改为带泛型的版本:

// 替换原来的非泛型Mock类型
late MockCollectionReference<Map<String, dynamic>> mockCollectionReference;
late MockDocumentReference<Map<String, dynamic>> mockDocumentReference;

// 在setUp中初始化
mockCollectionReference = MockCollectionReference<Map<String, dynamic>>();
mockDocumentReference = MockDocumentReference<Map<String, dynamic>>();

3. 验证存根类型匹配

此时when(mockFirebaseFirestore.collection(any)).thenReturn(mockCollectionReference)的返回类型会与实际代码中期望的CollectionReference<Map<String, dynamic>>完全匹配,类型错误即可解决。

注意:如果项目中使用withConverter将Firestore文档转换为自定义实体类,需要将泛型参数替换为对应的实体类类型。

内容的提问来源于stack exchange,提问作者Corner

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 02:45:54