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);
});
});
}



