# Unit Testing en Flutter
El unit testing es la prueba de unidades individuales de código (funciones, métodos, clases) de forma aislada.
## ¿Por qué Unit Testing?
- ✅ **Detecta errores temprano**
- ✅ **Documenta el comportamiento esperado**
- ✅ **Facilita refactoring seguro**
- ✅ **Mejora el diseño del código**
- ✅ **Reduce costos de mantenimiento**
## Configuración
```yaml
# pubspec.yaml
dev_dependencies:
flutter_test:
sdk: flutter
mockito: ^5.4.0
build_runner: ^2.4.0
```
## Anatomía de un Unit Test
El patrón AAA (Arrange-Act-Assert) es el estándar para escribir tests legibles.
```mermaid
graph LR
A[Arrange] -->|Preparar datos y mocks| B[Act]
B -->|Ejecutar acción| C[Assert]
C -->|Verificar resultado| D[Resultado]
style A fill:#e1f5fe,stroke:#01579b
style B fill:#fff3e0,stroke:#e65100
style C fill:#e8f5e9,stroke:#1b5e20
```
```dart
import 'package:flutter_test/flutter_test.dart';
void main() {
// Arrange, Act, Assert
test('Calculator adds two numbers correctly', () {
// Arrange - Preparar datos de prueba
final calculator = Calculator();
const a = 5;
const b = 3;
// Act - Ejecutar la acción
final result = calculator.add(a, b);
// Assert - Verificar el resultado
expect(result, 8);
});
}
```
## Testear Funciones Puras
```dart
// lib/utils/validators.dart
class Validators {
static bool isValidEmail(String email) {
final regex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
return regex.hasMatch(email);
}
static bool isValidPassword(String password) {
return password.length >= 8 &&
password.contains(RegExp(r'[A-Z]')) &&
password.contains(RegExp(r'[0-9]'));
}
}
// test/utils/validators_test.dart
import 'package:flutter_test/flutter_test.dart';
void main() {
group('Validators', () {
group('isValidEmail', () {
test('should return true for valid email', () {
expect(Validators.isValidEmail('[email protected]'), true);
expect(Validators.isValidEmail('[email protected]'), true);
});
test('should return false for invalid email', () {
expect(Validators.isValidEmail('invalidemail'), false);
expect(Validators.isValidEmail('test@'), false);
expect(Validators.isValidEmail('@example.com'), false);
expect(Validators.isValidEmail(''), false);
});
});
group('isValidPassword', () {
test('should return true for valid password', () {
expect(Validators.isValidPassword('Password123'), true);
expect(Validators.isValidPassword('MyP@ssw0rd'), true);
});
test('should return false for weak password', () {
expect(Validators.isValidPassword('short1A'), false); // Muy corto
expect(Validators.isValidPassword('nouppercase123'), false); // Sin mayúsculas
expect(Validators.isValidPassword('NoNumbers'), false); // Sin números
});
});
});
}
```
## Testear Clases con Dependencias (Mocking)
```dart
// lib/repositories/user_repository.dart
abstract class UserRepository {
Future getUser(String id);
Future saveUser(User user);
}
// lib/services/user_service.dart
class UserService {
final UserRepository repository;
UserService(this.repository);
Future getUserById(String id) async {
if (id.isEmpty) {
throw ArgumentError('User ID cannot be empty');
}
return await repository.getUser(id);
}
Future isAdult(String userId) async {
final user = await getUserById(userId);
return user.age >= 18;
}
}
// test/services/user_service_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'user_service_test.mocks.dart';
@GenerateMocks([UserRepository])
void main() {
late UserService userService;
late MockUserRepository mockRepository;
setUp(() {
mockRepository = MockUserRepository();
userService = UserService(mockRepository);
});
group('UserService', () {
group('getUserById', () {
const tUserId = '123';
final tUser = User(id: tUserId, name: 'Test User', age: 25);
test('should return User when repository returns user', () async {
// Arrange
when(mockRepository.getUser(tUserId))
.thenAnswer((_) async => tUser);
// Act
final result = await userService.getUserById(tUserId);
// Assert
expect(result, tUser);
verify(mockRepository.getUser(tUserId));
verifyNoMoreInteractions(mockRepository);
});
test('should throw ArgumentError when id is empty', () async {
// Act & Assert
expect(
() => userService.getUserById(''),
throwsA(isA()),
);
verifyNever(mockRepository.getUser(any));
});
test('should propagate exception from repository', () async {
// Arrange
when(mockRepository.getUser(tUserId))
.thenThrow(Exception('Network error'));
// Act & Assert
expect(
() => userService.getUserById(tUserId),
throwsException,
);
});
});
group('isAdult', () {
test('should return true when user age is 18 or more', () async {
// Arrange
final tUser = User(id: '1', name: 'Adult', age: 25);
when(mockRepository.getUser(any))
.thenAnswer((_) async => tUser);
// Act
final result = await userService.isAdult('1');
// Assert
expect(result, true);
});
test('should return false when user age is less than 18', () async {
// Arrange
final tUser = User(id: '1', name: 'Minor', age: 16);
when(mockRepository.getUser(any))
.thenAnswer((_) async => tUser);
// Act
final result = await userService.isAdult('1');
// Assert
expect(result, false);
});
});
});
}
```
## Testear BLoCs/Cubits
```dart
// lib/bloc/counter_bloc.dart
class CounterBloc extends Bloc {
CounterBloc() : super(0) {
on((event, emit) => emit(state + 1));
on((event, emit) => emit(state - 1));
on((event, emit) => emit(0));
}
}
// test/bloc/counter_bloc_test.dart
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('CounterBloc', () {
blocTest(
'emits [1] when Increment is added',
build: () => CounterBloc(),
act: (bloc) => bloc.add(Increment()),
expect: () => [1],
);
blocTest(
'emits [-1] when Decrement is added',
build: () => CounterBloc(),
act: (bloc) => bloc.add(Decrement()),
expect: () => [-1],
);
blocTest(
'emits [1, 2, 3] when three Increments are added',
build: () => CounterBloc(),
act: (bloc) {
bloc.add(Increment());
bloc.add(Increment());
bloc.add(Increment());
},
expect: () => [1, 2, 3],
);
blocTest(
'emits [0] when Reset is added after increments',
build: () => CounterBloc(),
seed: () => 5,
act: (bloc) => bloc.add(Reset()),
expect: () => [0],
);
});
}
```
## Test-Driven Development (TDD)
### Ciclo Red-Green-Refactor
```mermaid
graph TD
A[Red: Escribir test que falla] -->|Falla| B[Green: Escribir código mínimo]
B -->|Pasa| C[Refactor: Mejorar código]
C -->|Tests siguen pasando| A
style A fill:#ffcccc,stroke:#ff0000,stroke-width:2px
style B fill:#ccffcc,stroke:#00ff00,stroke-width:2px
style C fill:#ccccff,stroke:#0000ff,stroke-width:2px
```
1. **Red**: Escribe un test que falle
2. **Green**: Escribe el código mínimo para que pase
3. **Refactor**: Mejora el código manteniendo los tests verdes
```dart
// 1. RED - Escribir test que falla
test('Calculator should multiply two numbers', () {
final calculator = Calculator();
expect(calculator.multiply(3, 4), 12);
});
// ❌ Error: The method 'multiply' isn't defined
// 2. GREEN - Implementar código mínimo
class Calculator {
int multiply(int a, int b) => a * b;
}
// ✅ Test pasa
// 3. REFACTOR - Mejorar si es necesario
class Calculator {
int multiply(int a, int b) {
if (a == 0 || b == 0) return 0;
return a * b;
}
}
// ✅ Tests siguen pasando
```
## Matchers Comunes
```dart
void main() {
test('Common matchers', () {
// Igualdad
expect(1 + 1, 2);
expect(1 + 1, equals(2));
// Tipos
expect('hello', isA());
expect(42, isA());
// Booleanos
expect(true, isTrue);
expect(false, isFalse);
// Nulos
expect(null, isNull);
expect('not null', isNotNull);
// Contenido
expect([1, 2, 3], contains(2));
expect('hello world', contains('world'));
// Listas
expect([1, 2, 3], hasLength(3));
expect([1, 2, 3], containsAll([1, 3]));
// Rangos
expect(5, greaterThan(3));
expect(5, lessThan(10));
expect(5, closeTo(5.1, 0.2));
// Excepciones
expect(() => throw Exception(), throwsException);
expect(() => throw ArgumentError(), throwsA(isA()));
});
}
```
## Mejores Prácticas
### ✅ Hacer
```dart
// Buenos nombres descriptivos
test('should return error when email is invalid', () {});
test('should save user to database when valid data provided', () {});
// Un assert por test
test('should calculate total price correctly', () {
final total = cart.calculateTotal();
expect(total, 100.0);
});
// Usar setUp y tearDown
group('UserService tests', () {
late UserService service;
late MockRepository mockRepo;
setUp(() {
mockRepo = MockRepository();
service = UserService(mockRepo);
});
tearDown(() {
// Limpieza si es necesaria
});
test('...', () {});
});
// Testear casos límite
test('should handle empty list', () {});
test('should handle null values', () {});
test('should handle very large numbers', () {});
```
### ❌ Evitar
```dart
// ❌ Nombres vagos
test('test1', () {});
test('it works', () {});
// ❌ Múltiples asserts sin relación
test('user operations', () {
expect(user.name, 'John');
expect(user.age, 25);
expect(user.email, '[email protected]');
expect(user.isActive, true);
// Demasiado, separa en tests individuales
});
// ❌ Tests que dependen de otros
test('create user', () {
userId = service.createUser();
});
test('update user', () {
service.updateUser(userId); // ❌ Depende del test anterior
});
// ❌ Tests lentos
test('should process data', () async {
await Future.delayed(Duration(seconds: 10)); // ❌ Muy lento
});
```
## Coverage (Cobertura de Código)
```bash
# Ejecutar tests con coverage
flutter test --coverage
# Ver reporte HTML
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html
```
## Estructura de Archivos de Test
```
test/
├── unit/
│ ├── models/
│ │ └── user_test.dart
│ ├── services/
│ │ └── user_service_test.dart
│ └── utils/
│ └── validators_test.dart
├── widget/
│ └── ...
└── integration/
└── ...
```
## Conclusión
El unit testing es fundamental para:
- ✅ **Confianza en el código**
- ✅ **Desarrollo más rápido a largo plazo**
- ✅ **Documentación viva del comportamiento**
- ✅ **Facilitar refactoring**
> "El código sin tests es código legacy desde el día 1" - Michael Feathers
Este apartado profundiza en los conceptos clave, proporcionando ejemplos prácticos y mejores prácticas para su aplicación en proyectos reales.
Al comprender estos detalles, podrás diseñar soluciones más robustas, mantenibles y escalables en tus aplicaciones Flutter y Dart.