image

Acesse bootcamps ilimitados e +650 cursos pra sempre

75
%OFF
Article image
Daniel Passos
Daniel Passos04/12/2025 20:56
Compartilhe

Pare de Mockar o Firestore: Como sai do 'Mock Hell' e tornei meus testes indestutíveis.

    Quando estava finalizando os testes unitários de uma classe que implementei, deparei-me com uma série de Mocks. Uma série não, um verdadeiro emaranhado de Mocks. Não entendi nada; estava tudo intensamente confuso.

    Ao pesquisar e aprofundar-me no tema, recebi um diagnóstico preciso:

    "Você está testando a implementação interna da biblioteca, não a sua regra de negócios."

    O código problemático se parecia com isto:

    when(mockFirestore.collection(FirebaseCollections.users)).thenReturn(mockCollectionReference);
    when(mockCollectionReference.doc(any)).thenReturn(mockDocumentReference);
    when(mockDocumentSnapshot.exists).thenReturn(true);
    // ... e a lista continua
    

    Para solucionar esse problema, encontrei a resposta nos pacotes fake_cloud_firestore e firebase_auth_mocks.

    Por que esses pacotes foram superiores para o meu caso? Os "Fakes" simulam o comportamento real do banco em memória. Eles permitem testar estado (se o dado foi salvo e persiste), e não a implementação (se o método X ou Y foi chamado na ordem exata).

    No entanto, o mockito (pacote que utilizei inicialmente) não é inútil. Ele chega onde o Fake não vai. O mockito é capaz de simular catástrofes de infraestrutura que o Fake, por ser "perfeito" demais, não consegue reproduzir (como um erro de disco ou queda de rede).

    Veja como ficou a implementação de um teste de falha usando o Mockito cirurgicamente:

    test("Should throw UserRegistrationError when Firestore write fails", () async {
        // 1. PREPARAÇÃO (ARRANGE)
        // Usamos o Mockito aqui APENAS porque queremos forçar um erro que o Fake não simula.
        final mockFailingFirestore = MockFirebaseFirestorebyMockito();
        
        // ... Configuração dos mocks ...
    
        // Configurando a "armadilha" do Mock para falhar no .set()
        when(mockDocRef.set(any)).thenThrow(
          FirebaseException(plugin: 'firestore', message: 'Disk Error'),
        );
    
        // Injetamos a dependência "quebrada" (Firestore) e a "funcional" (Auth)
        final failingDataSource = AuthDataSource(
          firebaseAuth: mockAuth,
          firestore: mockFailingFirestore,
        );
    
        // 2. AÇÃO & 3. VERIFICAÇÃO (ACT & ASSERT)
        expect(
          () async => await failingDataSource.register(user, password),
          throwsA(isA<UserRegistrationError>()), 
        );
    });
    

    Diante disso, é possível inferir a regra de ouro: Fakes testam Lógica e Estados; Mocks testam Fronteiras e Falhas.

    A partir de agora, adoto essa estratégia híbrida, onde os Fakes validam o "caminho feliz" e a integridade dos dados, enquanto os Mocks garantem a robustez nos cenários de exceção.
    import 'package:app/core/error/auth_error.dart';
    import 'package:app/core/error/network_error.dart';
    import 'package:app/core/helpers/app_type.dart';
    import 'package:app/core/helpers/firebase_collections.dart';
    import 'package:app/features/domain/entities/user.dart' as model;
    import 'package:app/features/infra/sources/firebase/auth/auth_datasource.dart';
    import 'package:cloud_firestore/cloud_firestore.dart';
    import 'package:fake_cloud_firestore/fake_cloud_firestore.dart';
    import 'package:firebase_auth/firebase_auth.dart';
    import 'package:firebase_auth_mocks/firebase_auth_mocks.dart';
    import 'package:flutter_test/flutter_test.dart';
    import 'package:mockito/annotations.dart';
    import 'package:mockito/mockito.dart';
    
    
    // Mantenha o arquivo gerado
    import 'auth_datasource_test.mocks.dart';
    
    
    @GenerateMocks(
    [],
    customMocks: [
      MockSpec<FirebaseAuth>(as: #MockFirebaseAuthByMockito),
      MockSpec<FirebaseFirestore>(as: #MockFirebaseFirestorebyMockito),
      MockSpec<CollectionReference>(as: #MockCollectionReferencebyMockito),
      MockSpec<DocumentReference>(as: #MockDocumentReferencebyMockito),
      MockSpec<UserCredential>(as: #MockUserCredentialbyMockito),
    ],
    )
    void main() {
    late model.User user;
    const password = "GarageDaniel@777";
    
    
    setUpAll(() async {
      user = model.User(
        id: null,
        displayName: "Daniel",
        email: "daniel_mingozzi@hotmail.com",
        createdAt: DateTime.now(),
        role: AppType.driver,
      );
    });
    
    
    // ---------------------------------------------------------------------------
    // GRUPO 1: INFRASTRUCTURE FAILURES (Onde usamos MOCKITO)
    // Objetivo: Testar erros de hardware, rede ou plugins que os Fakes não simulam.
    // ---------------------------------------------------------------------------
    group("Infrastructure Failures (Mockito Strategy)", () {
      late AuthDataSource dataSource;
      late MockFirebaseFirestorebyMockito mockFirestore;
      late MockCollectionReferencebyMockito<Map<String, dynamic>> mockCollection;
      late MockDocumentReferencebyMockito<Map<String, dynamic>> mockDocRef;
      // Usamos o MockAuth do Mockito aqui para ter controle total das exceções
      late MockFirebaseAuthByMockito mockAuth; 
      
      setUp(() {
        mockFirestore = MockFirebaseFirestorebyMockito();
        mockAuth = MockFirebaseAuthByMockito();
        mockCollection = MockCollectionReferencebyMockito();
        mockDocRef = MockDocumentReferencebyMockito();
    
    
        dataSource = AuthDataSource(
          firebaseAuth: mockAuth,
          firestore: mockFirestore,
        );
      });
    
    
      test("Should throw UserRegistrationError when Firestore write fails (Disk Error)", () async {
        // ARRANGE
        // 1. O Auth funciona
        when(mockAuth.createUserWithEmailAndPassword(
          email: anyNamed('email'),
          password: anyNamed('password'),
        )).thenAnswer((_) async => MockUserCredentialbyMockito()); // Retorna sucesso falso
    
    
        // 2. O Firestore FALHA ao tentar salvar
        when(mockFirestore.collection(any)).thenReturn(mockCollection);
        when(mockCollection.doc(any)).thenReturn(mockDocRef);
        when(mockDocRef.set(any)).thenThrow(
          FirebaseException(plugin: 'firestore', message: 'Disk Error'),
        );
    
    
        // ACT & ASSERT
        expect(
          () async => await dataSource.register(user, password),
          throwsA(isA<UserRegistrationError>()),
        );
      });
      
      test("Should throw NoInternetConnectionError when Auth fails", () async {
          // ARRANGE
          when(mockAuth.createUserWithEmailAndPassword(
              email: anyNamed('email'), 
              password: anyNamed('password')
          )).thenThrow(FirebaseAuthException(code: 'network-request-failed'));
    
    
          // ACT & ASSERT
          expect(
              () async => await dataSource.register(user, password),
              throwsA(isA<NoInternetConnectionError>()),
          );
      });
    });
    
    
    // ---------------------------------------------------------------------------
    // GRUPO 2: LOGIC & STATE (Onde usamos FAKES)
    // Objetivo: Testar o fluxo de dados real, persistência e estado.
    // ---------------------------------------------------------------------------
    group("Happy Path & Logic (Fakes Strategy)", () {
      late AuthDataSource dataSource;
      late MockFirebaseAuth firebaseAuth; // Esse é o fake do package firebase_auth_mocks
      late FakeFirebaseFirestore firestore;
    
    
      setUp(() async {
        firebaseAuth = MockFirebaseAuth();
        firestore = FakeFirebaseFirestore();
        dataSource = AuthDataSource(
          firebaseAuth: firebaseAuth,
          firestore: firestore,
        );
      });
    
    
      // Helper para reduzir repetição de código
      Future<model.User?> createAndLoginUser() async {
        // Cria no Auth
        final UserCredential userCredential = await firebaseAuth
            .createUserWithEmailAndPassword(email: user.email, password: password);
        
        // Atualiza ID local
        final userWithId = user.copyWith(id: userCredential.user?.uid);
        
        // Salva no Firestore (Simulando o fluxo real)
        await firestore
            .collection(FirebaseCollections.users)
            .doc(userWithId.id)
            .set(userWithId.toMap());
            
        return await dataSource.login(userWithId.email, password);
      }
    
    
      test('should register user and save to firestore correctly', () async {
        final result = await dataSource.register(user, password);
        expect(result, true);
        
        // Verificação de ESTADO (Vantagem do Fake):
        // O dado realmente está lá?
        final savedUser = await firestore
            .collection(FirebaseCollections.users)
            .get()
            .then((snapshot) => snapshot.docs.first);
            
        expect(savedUser.data()['email'], user.email);
      });
    
    
      test("should make login and retrieve full user data", () async {
        final result = await createAndLoginUser();
        
        expect(result, isNotNull);
        expect(result!.email, user.email);
        expect(result.id, isNotNull);
      });
    
    
      test("should get current user from persistence", () async {
        await createAndLoginUser();
        
        final result = await dataSource.getCurrentUser();
        
        expect(result, isNotNull);
        expect(result!.email, user.email);
      });
    
    
      test("should logout user", () async {
        await createAndLoginUser();
        
        await dataSource.logout();
        
        final currentUser = await dataSource.getCurrentUser();
        expect(currentUser, isNull);
      });
    
    
      test("should deactivate account (logic check)", () async {
        await createAndLoginUser();
        
        final isDeactivated = await dataSource.deactivateAccount();
        
        expect(isDeactivated, true);
        
        // Verificação de ESTADO:
        // O usuário foi deslogado?
        expect(firebaseAuth.currentUser, isNull);
        
        // O campo no banco foi atualizado? (Você precisaria buscar o doc pelo ID para validar isso no Fake)
      });
    
    
      test("isAuthenticated returns correct boolean state", () async {
        await createAndLoginUser();
        expect(dataSource.isAuthenticated(), true);
        
        await dataSource.logout();
        expect(dataSource.isAuthenticated(), false);
      });
    });
    }
    
    Compartilhe
    Recomendados para você
    GitHub Copilot - Código na Prática
    CI&T - Backend com Java & AWS
    Nexa - Machine Learning e GenAI na Prática
    Comentários (0)