Flutter viene con un framework de testing completo incluido en el SDK. No hace falta instalar nada adicional para escribir unit tests, widget tests e integration tests. A pesar de eso, el testing sigue siendo una de las partes más descuidadas en los proyectos Flutter. Este artículo cubre los tres tipos con ejemplos concretos.
Unit tests: lógica pura
Los unit tests en Flutter son tests de Dart estándar. Prueban funciones, clases y lógica de negocio sin widgets ni framework de UI. El paquete flutter_test los incluye, aunque para tests puramente de Dart puedes usar directamente test.
// lib/utils/calculator.dart
class Calculator {
double add(double a, double b) => a + b;
double divide(double a, double b) {
if (b == 0) throw ArgumentError('No se puede dividir por cero');
return a / b;
}
}
// test/utils/calculator_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/utils/calculator.dart';
void main() {
group('Calculator', () {
late Calculator calc;
setUp(() {
calc = Calculator();
});
test('suma dos números', () {
expect(calc.add(2, 3), equals(5.0));
});
test('divide correctamente', () {
expect(calc.divide(10, 2), equals(5.0));
});
test('lanza excepción al dividir por cero', () {
expect(() => calc.divide(10, 0), throwsArgumentError);
});
});
}
Para ejecutar los tests:
flutter test test/utils/calculator_test.dart # O todos los tests del proyecto flutter test
Mockear dependencias con Mockito o Mocktail
Cuando el código que quieres probar tiene dependencias externas (una API, una base de datos, un servicio), necesitas mocks. Las dos opciones más habituales en el ecosistema Flutter son mockito (con code generation) y mocktail (sin generación, más sencillo de configurar).
// Con mocktail
import 'package:mocktail/mocktail.dart';
import 'package:flutter_test/flutter_test.dart';
abstract class UserRepository {
Future<String> getUsername(int id);
}
class MockUserRepository extends Mock implements UserRepository {}
void main() {
test('obtiene nombre de usuario', () async {
final repo = MockUserRepository();
when(() => repo.getUsername(42)).thenAnswer((_) async => 'Ana');
final name = await repo.getUsername(42);
expect(name, 'Ana');
verify(() => repo.getUsername(42)).called(1);
});
}
Widget tests: la UI sin dispositivo
Los widget tests permiten probar widgets de Flutter en un entorno simulado, sin necesidad de un emulador o dispositivo real. Son más lentos que los unit tests pero mucho más rápidos que los integration tests.
// test/widgets/counter_widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/widgets/counter_widget.dart';
void main() {
testWidgets('el contador incrementa al pulsar el botón', (tester) async {
await tester.pumpWidget(
const MaterialApp(home: CounterWidget()),
);
// Verifica el estado inicial
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Pulsa el botón
await tester.tap(find.byIcon(Icons.add));
await tester.pump(); // reconstruye el widget
// Verifica el nuevo estado
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
testWidgets('muestra texto correcto', (tester) async {
await tester.pumpWidget(
const MaterialApp(home: CounterWidget()),
);
expect(find.text('Contador'), findsOneWidget);
});
}
tester.pump() dispara un frame de renderizado. Si el widget hace operaciones asíncronas, usa tester.pumpAndSettle() para esperar a que terminen las animaciones y los futures pendientes.
Buscar widgets en el árbol
Los finders de Flutter son muy expresivos:
find.text('Hola') // por texto
find.byType(ElevatedButton) // por tipo de widget
find.byKey(Key('mi_key')) // por clave
find.byIcon(Icons.add) // por icono
find.descendant( // anidado
of: find.byType(Card),
matching: find.byType(Text),
)
Integration tests: el escenario real
Los integration tests corren en un dispositivo o emulador real. Usan el paquete integration_test, que viene en el SDK de Flutter, y permiten probar flujos completos de la app.
// integration_test/app_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('flujo de login completo', (tester) async {
app.main();
await tester.pumpAndSettle();
// Encuentra el campo de email y escribe
await tester.enterText(find.byKey(const Key('email_field')), '[email protected]');
await tester.enterText(find.byKey(const Key('password_field')), 'password123');
// Pulsa el botón de login
await tester.tap(find.text('Entrar'));
await tester.pumpAndSettle();
// Verifica que llegamos a la pantalla principal
expect(find.text('Bienvenido'), findsOneWidget);
});
}
Para ejecutar integration tests:
flutter test integration_test/app_test.dart
Qué probar en cada nivel
La regla general es la pirámide de tests: muchos unit tests (rápidos y baratos), un número razonable de widget tests y pocos integration tests (lentos y costosos de mantener). En Flutter, los widget tests ocupan el lugar de los tests de integración de nivel medio porque son más rápidos que en otros frameworks.
Para la lógica de negocio y los providers/blocs, unit tests. Para los widgets con algo de lógica de UI, widget tests. Para los flujos críticos de la app (login, pago, navegación principal), integration tests con los escenarios más importantes.
Si usas Riverpod, también puedes probar los providers directamente con ProviderContainer en unit tests, sin necesidad de montar widgets. Si usas Bloc, bloc_test es el paquete estándar para probar eventos y estados de forma declarativa.
Para ver cómo encajan los tests en un pipeline de CI/CD completo, puedes consultar el artículo sobre Flutter en producción con GitHub Actions y Fastlane.
Imagen: Pexels / _Karub_ ?
