Overview
Comprehensive testing ensures the Restaurant Reservation System is reliable, maintainable, and bug-free. This guide covers unit tests, widget tests, integration tests, and Firebase testing strategies.Testing Stack
The project uses these testing packages:pubspec.yaml
dev_dependencies:
flutter_test:
sdk: flutter
bloc_test: ^9.1.0 # Test Cubits/Blocs
mocktail: ^1.0.0 # Mocking dependencies
fake_cloud_firestore: ^2.4.0 # Mock Firestore
firebase_auth_mocks: ^0.13.0 # Mock Firebase Auth
flutter pub get
Unit Testing
Testing Use Cases
Test business logic in isolation:test/aplicacion/crear_reserva_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:app_restaurante/aplicacion/crear_reserva.dart';
import 'package:app_restaurante/dominio/entidades/reserva.dart';
import 'package:app_restaurante/dominio/repositorios/reserva_repositorio.dart';
// Create mocks
class MockReservaRepositorio extends Mock implements ReservaRepositorio {}
class MockMesaRepositorio extends Mock implements MesaRepositorio {}
class MockNegocioRepositorio extends Mock implements NegocioRepositorio {}
void main() {
late CrearReserva crearReserva;
late MockReservaRepositorio mockReservaRepo;
late MockMesaRepositorio mockMesaRepo;
late MockNegocioRepositorio mockNegocioRepo;
setUp(() {
mockReservaRepo = MockReservaRepositorio();
mockMesaRepo = MockMesaRepositorio();
mockNegocioRepo = MockNegocioRepositorio();
crearReserva = CrearReserva(
mockReservaRepo,
mesaRepositorio: mockMesaRepo,
negocioRepositorio: mockNegocioRepo,
);
});
group('CrearReserva', () {
test('should create reservation successfully when all validations pass', () async {
// Arrange
final fecha = DateTime.now().add(Duration(days: 1));
final hora = DateTime(fecha.year, fecha.month, fecha.day, 20, 0);
final mesa = Mesa(
id: 'mesa1',
nombre: 'Mesa VIP-1',
capacidad: 4,
zona: 'VIP',
negocioId: 'negocio1',
);
final negocio = Negocio(
id: 'negocio1',
nombre: 'Mi Restaurante',
duracionPromedioMinutos: 60,
maxDiasAnticipacionReserva: 14,
);
when(() => mockMesaRepo.obtenerMesaPorId('mesa1'))
.thenAnswer((_) async => mesa);
when(() => mockNegocioRepo.obtenerNegocioPorId('negocio1'))
.thenAnswer((_) async => negocio);
when(() => mockReservaRepo.mesaDisponible(
mesaId: any(named: 'mesaId'),
fecha: any(named: 'fecha'),
hora: any(named: 'hora'),
duracionMinutos: any(named: 'duracionMinutos'),
)).thenAnswer((_) async => true);
when(() => mockReservaRepo.crearReserva(any()))
.thenAnswer((invocation) async {
final reserva = invocation.positionalArguments[0] as Reserva;
return reserva.copyWith(id: 'reserva123');
});
// Act
final resultado = await crearReserva.ejecutar(
'mesa1',
fecha,
hora,
4,
contactoCliente: '[email protected]',
nombreCliente: 'Juan Pérez',
negocioId: 'negocio1',
);
// Assert
expect(resultado.id, 'reserva123');
expect(resultado.mesaId, 'mesa1');
expect(resultado.numeroPersonas, 4);
verify(() => mockReservaRepo.crearReserva(any())).called(1);
});
test('should throw exception when date is in the past', () async {
// Arrange
final fecha = DateTime.now().subtract(Duration(days: 1));
final hora = DateTime(fecha.year, fecha.month, fecha.day, 20, 0);
// Act & Assert
expect(
() => crearReserva.ejecutar(
'mesa1',
fecha,
hora,
4,
negocioId: 'negocio1',
),
throwsA(isA<Exception>()),
);
});
test('should throw exception when table is not available', () async {
// Arrange
final fecha = DateTime.now().add(Duration(days: 1));
final hora = DateTime(fecha.year, fecha.month, fecha.day, 20, 0);
when(() => mockMesaRepo.obtenerMesaPorId(any()))
.thenAnswer((_) async => Mesa(
id: 'mesa1',
nombre: 'Mesa 1',
capacidad: 4,
zona: 'Salón',
negocioId: 'negocio1',
));
when(() => mockReservaRepo.mesaDisponible(
mesaId: any(named: 'mesaId'),
fecha: any(named: 'fecha'),
hora: any(named: 'hora'),
duracionMinutos: any(named: 'duracionMinutos'),
)).thenAnswer((_) async => false);
// Act & Assert
expect(
() => crearReserva.ejecutar(
'mesa1',
fecha,
hora,
4,
negocioId: 'negocio1',
),
throwsA(predicate((e) =>
e is Exception &&
e.toString().contains('ya está reservada')
)),
);
});
});
}
Testing Repositories
Test Firestore adapters with fake Firestore:test/adaptadores/adaptador_firestore_reserva_test.dart
import 'package:fake_cloud_firestore/fake_cloud_firestore.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:app_restaurante/adaptadores/adaptador_firestore_reserva.dart';
import 'package:app_restaurante/dominio/entidades/reserva.dart';
void main() {
late FakeFirebaseFirestore fakeFirestore;
late ReservaRepositorioFirestore repositorio;
setUp(() {
fakeFirestore = FakeFirebaseFirestore();
repositorio = ReservaRepositorioFirestore(firestore: fakeFirestore);
});
group('ReservaRepositorioFirestore', () {
test('should create a reservation in Firestore', () async {
// Arrange
final reserva = Reserva(
id: '',
mesaId: 'mesa1',
fechaHora: DateTime(2024, 12, 25, 20, 0),
numeroPersonas: 4,
duracionMinutos: 60,
estado: EstadoReserva.confirmada,
contactoCliente: '[email protected]',
nombreCliente: 'María García',
negocioId: 'negocio1',
);
// Act
final resultado = await repositorio.crearReserva(reserva);
// Assert
expect(resultado.id, isNotEmpty);
// Verify in Firestore
final snapshot = await fakeFirestore.collection('reservas').get();
expect(snapshot.docs.length, 1);
expect(snapshot.docs.first.data()['mesaId'], 'mesa1');
expect(snapshot.docs.first.data()['numeroPersonas'], 4);
});
test('should retrieve reservations by table and date', () async {
// Arrange
final fecha = DateTime(2024, 12, 25);
await fakeFirestore.collection('reservas').add({
'mesaId': 'mesa1',
'fechaHora': Timestamp.fromDate(DateTime(2024, 12, 25, 20, 0)),
'numeroPersonas': 4,
'estado': 'confirmada',
'duracionMinutos': 60,
});
await fakeFirestore.collection('reservas').add({
'mesaId': 'mesa1',
'fechaHora': Timestamp.fromDate(DateTime(2024, 12, 25, 21, 0)),
'numeroPersonas': 2,
'estado': 'confirmada',
'duracionMinutos': 60,
});
// Act
final reservas = await repositorio.obtenerReservasPorMesaYHorario(
mesaId: 'mesa1',
fecha: fecha,
hora: DateTime(2024, 12, 25, 20, 0),
);
// Assert
expect(reservas.length, 2);
expect(reservas.every((r) => r.mesaId == 'mesa1'), true);
});
});
}
Testing Cubits
Usebloc_test package to test state emissions:
test/presentacion/disponibilidad_cubit_test.dart
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:app_restaurante/presentacion/disponibilidad/disponibilidad_cubit.dart';
import 'package:app_restaurante/presentacion/disponibilidad/disponibilidad_estados_de_cubit.dart';
class MockMesaRepositorio extends Mock implements MesaRepositorio {}
class MockNegocioRepositorio extends Mock implements NegocioRepositorio {}
class MockCrearReserva extends Mock implements CrearReserva {}
void main() {
late MockMesaRepositorio mockMesaRepo;
late MockNegocioRepositorio mockNegocioRepo;
late MockCrearReserva mockCrearReserva;
setUp(() {
mockMesaRepo = MockMesaRepositorio();
mockNegocioRepo = MockNegocioRepositorio();
mockCrearReserva = MockCrearReserva();
});
group('DisponibilidadCubit', () {
test('initial state is DisponibilidadInicial', () {
final cubit = DisponibilidadCubit(
mesaRepositorio: mockMesaRepo,
negocioRepositorio: mockNegocioRepo,
crearReserva: mockCrearReserva,
);
expect(cubit.state, isA<DisponibilidadInicial>());
});
blocTest<DisponibilidadCubit, DisponibilidadState>(
'emits [Cargando, Exitosa] when cargarTodasLasMesas succeeds',
build: () {
when(() => mockMesaRepo.obtenerMesasPorNegocio(any()))
.thenAnswer((_) async => [
Mesa(id: '1', nombre: 'Mesa 1', capacidad: 4, zona: 'Salón', negocioId: 'n1'),
Mesa(id: '2', nombre: 'Mesa 2', capacidad: 2, zona: 'Terraza', negocioId: 'n1'),
]);
when(() => mockNegocioRepo.obtenerNegocioPorId(any()))
.thenAnswer((_) async => Negocio(
id: 'n1',
nombre: 'Mi Restaurante',
duracionPromedioMinutos: 60,
));
return DisponibilidadCubit(
mesaRepositorio: mockMesaRepo,
negocioRepositorio: mockNegocioRepo,
crearReserva: mockCrearReserva,
);
},
act: (cubit) => cubit.cargarTodasLasMesas('n1'),
expect: () => [
isA<DisponibilidadCargando>(),
isA<DisponibilidadExitosa>()
.having((s) => s.mesasDisponibles.length, 'mesa count', 2),
],
verify: (_) {
verify(() => mockMesaRepo.obtenerMesasPorNegocio('n1')).called(1);
verify(() => mockNegocioRepo.obtenerNegocioPorId('n1')).called(1);
},
);
blocTest<DisponibilidadCubit, DisponibilidadState>(
'emits [Cargando, Error] when repository throws error',
build: () {
when(() => mockMesaRepo.obtenerMesasPorNegocio(any()))
.thenThrow(Exception('Network error'));
return DisponibilidadCubit(
mesaRepositorio: mockMesaRepo,
negocioRepositorio: mockNegocioRepo,
crearReserva: mockCrearReserva,
);
},
act: (cubit) => cubit.cargarTodasLasMesas('n1'),
expect: () => [
isA<DisponibilidadCargando>(),
isA<DisponibilidadConError>()
.having((s) => s.mensaje, 'error message', contains('Network error')),
],
);
});
}
Widget Testing
Test UI components in isolation:test/widgets/tarjeta_mesa_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:app_restaurante/presentacion/widgets_comunes/tarjeta_mesa.dart';
void main() {
testWidgets('TarjetaMesa displays table information', (tester) async {
// Arrange
final mesa = Mesa(
id: '1',
nombre: 'Mesa VIP-1',
capacidad: 4,
zona: 'VIP',
negocioId: 'n1',
);
// Act
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: TarjetaMesa(mesa: mesa),
),
),
);
// Assert
expect(find.text('Mesa VIP-1'), findsOneWidget);
expect(find.text('Capacidad: 4'), findsOneWidget);
expect(find.text('VIP'), findsOneWidget);
});
testWidgets('TarjetaMesa calls onTap when tapped', (tester) async {
// Arrange
bool wasTapped = false;
final mesa = Mesa(
id: '1',
nombre: 'Mesa 1',
capacidad: 2,
zona: 'Salón',
negocioId: 'n1',
);
// Act
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: TarjetaMesa(
mesa: mesa,
onTap: () => wasTapped = true,
),
),
),
);
await tester.tap(find.byType(TarjetaMesa));
await tester.pump();
// Assert
expect(wasTapped, true);
});
}
Integration Testing
Test complete flows:integration_test/reserva_flow_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:app_restaurante/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Reservation Flow', () {
testWidgets('Complete reservation creation flow', (tester) async {
// Start app
app.main();
await tester.pumpAndSettle();
// Navigate to availability screen
await tester.tap(find.text('Hacer Reserva'));
await tester.pumpAndSettle();
// Select date
await tester.tap(find.byIcon(Icons.calendar_today));
await tester.pumpAndSettle();
// Select tomorrow's date
final tomorrow = DateTime.now().add(Duration(days: 1));
await tester.tap(find.text(tomorrow.day.toString()));
await tester.tap(find.text('OK'));
await tester.pumpAndSettle();
// Select table
await tester.tap(find.text('Mesa VIP-1').first);
await tester.pumpAndSettle();
// Fill customer information
await tester.enterText(
find.byType(TextField).first,
'Juan Pérez',
);
await tester.enterText(
find.byType(TextField).at(1),
'[email protected]',
);
// Submit reservation
await tester.tap(find.text('Confirmar Reserva'));
await tester.pumpAndSettle(Duration(seconds: 3));
// Verify success message
expect(find.text('Reserva confirmada'), findsOneWidget);
});
});
}
flutter test integration_test/reserva_flow_test.dart
Test Coverage
Generate coverage report:# Run tests with coverage
flutter test --coverage
# Generate HTML report (requires lcov)
genhtml coverage/lcov.info -o coverage/html
# Open report
open coverage/html/index.html
Aim for 80%+ code coverage on business logic (use cases, repositories, cubits).
Mocking Firebase Services
Mock Firebase Auth
import 'package:firebase_auth_mocks/firebase_auth_mocks.dart';
final mockAuth = MockFirebaseAuth(
mockUser: MockUser(
uid: 'user123',
email: '[email protected]',
displayName: 'Test User',
),
);
final user = await mockAuth.signInWithEmailAndPassword(
email: '[email protected]',
password: 'password123',
);
expect(user.user?.uid, 'user123');
Mock Firestore
import 'package:fake_cloud_firestore/fake_cloud_firestore.dart';
final fakeFirestore = FakeFirebaseFirestore();
// Add test data
await fakeFirestore.collection('reservas').add({
'mesaId': 'mesa1',
'numeroPersonas': 4,
});
// Query data
final snapshot = await fakeFirestore.collection('reservas').get();
expect(snapshot.docs.length, 1);
Best Practices
Test Organization
test/
├── aplicacion/ # Use case tests
│ ├── crear_reserva_test.dart
│ └── cancelar_reserva_test.dart
├── adaptadores/ # Repository tests
│ ├── adaptador_firestore_reserva_test.dart
│ └── servicio_email_test.dart
├── presentacion/ # Cubit tests
│ ├── disponibilidad_cubit_test.dart
│ └── pantalla_dueno_cubit_test.dart
├── widgets/ # Widget tests
│ └── tarjeta_mesa_test.dart
└── integration_test/ # Integration tests
└── reserva_flow_test.dart
Testing Checklist
- ✅ Test happy path (success scenarios)
- ✅ Test error cases (failures, exceptions)
- ✅ Test edge cases (empty lists, null values)
- ✅ Test validation logic
- ✅ Mock external dependencies (Firebase, APIs)
- ✅ Verify state emissions in correct order
- ✅ Check that repositories are called with correct parameters
- ✅ Test widget rendering and interactions
Common Pitfalls
Don’t forget to call
registerFallbackValue() for complex types when using Mocktail!setUpAll(() {
registerFallbackValue(Reserva(
id: '',
mesaId: '',
fechaHora: DateTime.now(),
numeroPersonas: 1,
duracionMinutos: 60,
estado: EstadoReserva.pendiente,
negocioId: '',
));
});
Continuous Integration
Add to.github/workflows/test.yml:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.7.2'
- name: Install dependencies
run: flutter pub get
- name: Run tests
run: flutter test --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: coverage/lcov.info
Next Steps
- Firebase Setup - Configure Firebase for testing
- State Management - Understand BLoC patterns
- Email Configuration - Test email flows