Módulo 01: Fundamentos y Buenas Prácticas

Integration Testing

Testing Strategy

# Integration Testing en Flutter El integration testing prueba cómo funcionan juntos múltiples componentes de la aplicación, incluyendo servicios externos, bases de datos y la UI completa. ## ¿Qué es Integration Testing? - Prueba flujos completos end-to-end - Se ejecuta en dispositivos reales o emuladores - Más lento pero más cercano al uso real - Detecta problemas de integración entre componentes ## Configuración ```yaml # pubspec.yaml dev_dependencies: integration_test: sdk: flutter flutter_test: sdk: flutter ``` ```dart // test_driver/integration_test.dart import 'package:integration_test/integration_test_driver.dart'; Future main() => integrationDriver(); ``` ## Estructura de Archivos ``` my_app/ ├── integration_test/ │ ├── app_test.dart │ ├── login_flow_test.dart │ └── checkout_flow_test.dart ├── test_driver/ │ └── integration_test.dart └── lib/ └── ... ``` ## Ejemplo Básico ```dart // 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('complete app flow', (tester) async { app.main(); await tester.pumpAndSettle(); // Tu test aquí expect(find.text('Welcome'), findsOneWidget); }); } ``` ## Ejecutar Integration Tests ```mermaid graph TD subgraph Host Machine Driver[Integration Driver] end subgraph Device/Emulator App[Flutter App] Test[Test Code] end Driver -->|Controla| Test Test -->|Interactúa con| App App -->|Retorna resultados| Driver style Host Machine fill:#e3f2fd,stroke:#2196f3 style Device/Emulator fill:#f1f8e9,stroke:#8bc34a ``` ```bash # En dispositivo/emulador flutter test integration_test/app_test.dart # Con driver flutter drive \ --driver=test_driver/integration_test.dart \ --target=integration_test/app_test.dart # En dispositivo específico flutter test integration_test/app_test.dart -d ``` ## Ejemplo Completo: Login Flow ```dart // integration_test/login_flow_test.dart import 'package:flutter/material.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(); group('Login Flow', () { testWidgets('successful login navigates to home', (tester) async { app.main(); await tester.pumpAndSettle(); // 1. Verificar pantalla de login expect(find.text('Login'), findsOneWidget); expect(find.byType(TextField), findsNWidgets(2)); // 2. Ingresar credenciales await tester.enterText( find.byKey(Key('email-field')), '[email protected]', ); await tester.enterText( find.byKey(Key('password-field')), 'password123', ); await tester.pumpAndSettle(); // 3. Tap en botón de login await tester.tap(find.byKey(Key('login-button'))); await tester.pumpAndSettle(Duration(seconds: 5)); // Esperar navegación y carga // 4. Verificar que navegó a home expect(find.byType(HomePage), findsOneWidget); expect(find.text('Welcome, Test User'), findsOneWidget); }); testWidgets('invalid credentials shows error', (tester) async { app.main(); await tester.pumpAndSettle(); // Ingresar credenciales inválidas await tester.enterText( find.byKey(Key('email-field')), '[email protected]', ); await tester.enterText( find.byKey(Key('password-field')), 'wrongpassword', ); // Submit await tester.tap(find.byKey(Key('login-button'))); await tester.pumpAndSettle(Duration(seconds: 3)); // Verificar error expect(find.text('Invalid credentials'), findsOneWidget); expect(find.byType(LoginPage), findsOneWidget); // Sigue en login }); }); } ``` ## Testear API Calls ```dart testWidgets('fetches and displays user data', (tester) async { app.main(); await tester.pumpAndSettle(); // Navegar a la pantalla de perfil await tester.tap(find.byIcon(Icons.person)); await tester.pumpAndSettle(); // Esperar a que cargue loading indicator expect(find.byType(CircularProgressIndicator), findsOneWidget); // Esperar a que cargue la data real await tester.pumpAndSettle(Duration(seconds: 5)); // Verificar datos cargados expect(find.text('John Doe'), findsOneWidget); expect(find.text('[email protected]'), findsOneWidget); }); ``` ## Testear Scrolling y Lists ```dart testWidgets('scrolls through product list', (tester) async { app.main(); await tester.pumpAndSettle(); // Verificar primer item visible expect(find.text('Product 1'), findsOneWidget); expect(find.text('Product 20'), findsNothing); // Scroll hasta el final final listFinder = find.byType(ListView); await tester.fling(listFinder, Offset(0, -3000), 10000); await tester.pumpAndSettle(); // Verificar último item visible expect(find.text('Product 1'), findsNothing); expect(find.text('Product 20'), findsOneWidget); // Tap en producto await tester.tap(find.text('Product 20')); await tester.pumpAndSettle(); // Verificar navegación a detalle expect(find.byType(ProductDetailPage), findsOneWidget); }); ``` ## Testear Formularios Complejos ```dart testWidgets('checkout flow completes successfully', (tester) async { app.main(); await tester.pumpAndSettle(); // 1. Agregar productos al carrito await tester.tap(find.text('Add to Cart').first); await tester.pumpAndSettle(); // 2. Ir al carrito await tester.tap(find.byIcon(Icons.shopping_cart)); await tester.pumpAndSettle(); expect(find.text('Cart'), findsOneWidget); expect(find.byType(CartItem), findsOneWidget); // 3. Proceder al checkout await tester.tap(find.text('Checkout')); await tester.pumpAndSettle(); // 4. Llenar información de envío await tester.enterText(find.byKey(Key('address-field')), '123 Main St'); await tester.enterText(find.byKey(Key('city-field')), 'New York'); await tester.enterText(find.byKey(Key('zip-field')), '10001'); // 5. Continuar a pago await tester.tap(find.text('Continue to Payment')); await tester.pumpAndSettle(); // 6. Ingresar información de pago await tester.enterText(find.byKey(Key('card-number')), '4242424242424242'); await tester.enterText(find.byKey(Key('card-expiry')), '12/25'); await tester.enterText(find.byKey(Key('card-cvc')), '123'); // 7. Confirmar orden await tester.tap(find.text('Place Order')); await tester.pumpAndSettle(Duration(seconds: 5)); // 8. Verificar confirmación expect(find.text('Order Confirmed'), findsOneWidget); expect(find.textContaining('Order #'), findsOneWidget); }); ``` ## Mock Services para Integration Tests ```dart // test/mocks/mock_api_client.dart class MockApiClient implements ApiClient { @override Future login(String email, String password) async { await Future.delayed(Duration(seconds: 1)); // Simular latencia if (email == '[email protected]' && password == 'password123') { return User(id: '1', name: 'Test User', email: email); } throw Exception('Invalid credentials'); } @override Future> getProducts() async { await Future.delayed(Duration(milliseconds: 500)); return List.generate( 20, (i) => Product(id: '$i', name: 'Product ${i + 1}', price: 10.0 * (i + 1)), ); } } // integration_test/app_test.dart con mocks void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); setUp(() { // Inyectar mock getIt.registerSingleton(MockApiClient()); }); testWidgets('app works with mocked API', (tester) async { app.main(); await tester.pumpAndSettle(); // Tests... }); } ``` ## Performance Testing ```dart testWidgets('app performance test', (tester) async { // Binding para métricas de performance final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized() as IntegrationTestWidgetsFlutterBinding; binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.benchmarkLive; app.main(); await tester.pumpAndSettle(); // Iniciar timeline recording await binding.traceAction(() async { // Navegar entre pantallas await tester.tap(find.text('Products')); await tester.pumpAndSettle(); // Scroll await tester.fling(find.byType(ListView), Offset(0, -500), 10000); await tester.pumpAndSettle(); // Tap en item await tester.tap(find.text('Product 5')); await tester.pumpAndSettle(); }, reportKey: 'navigation_performance'); // Las métricas se guardan automáticamente }); ``` ```bash # Ejecutar con perfil de performance flutter drive \ --driver=test_driver/perf_driver.dart \ --target=integration_test/perf_test.dart \ --profile ``` ## Screenshot Testing ```dart testWidgets('take screenshots of key screens', (tester) async { final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized() as IntegrationTestWidgetsFlutterBinding; app.main(); await tester.pumpAndSettle(); // Home screen await binding.takeScreenshot('home-screen'); // Navigate to products await tester.tap(find.text('Products')); await tester.pumpAndSettle(); await binding.takeScreenshot('products-screen'); // Product detail await tester.tap(find.text('Product 1')); await tester.pumpAndSettle(); await binding.takeScreenshot('product-detail-screen'); }); ``` ## Testear Different Platforms ```dart import 'dart:io'; testWidgets('platform-specific behavior', (tester) async { app.main(); await tester.pumpAndSettle(); if (Platform.isAndroid) { // Verificar drawer de Android expect(find.byType(Drawer), findsOneWidget); } else if (Platform.isIOS) { // Verificar navegación de iOS expect(find.byType(CupertinoNavigationBar), findsOneWidget); } }); ``` ## Mejores Prácticas ### ✅ Hacer ```dart // Usar keys para elementos importantes ElevatedButton( key: Key('login-button'), child: Text('Login'), ); // Timeouts generosos await tester.pumpAndSettle(Duration(seconds: 10)); // Tests idempotentes setUp(() { // Limpiar estado antes de cada test resetDatabase(); }); // Agrupar tests relacionados group('E-commerce Flow', () { testWidgets('browse products', ...); testWidgets('add to cart', ...); testWidgets('checkout', ...); }); ``` ### ❌ Evitar ```dart // ❌ Tests que dependen de orden test('create user'); // Crea usuario test('update user'); // Depende del anterior // ❌ Hard-coded waits await Future.delayed(Duration(seconds: 5)); // Usar: await tester.pumpAndSettle(); // ❌ Tests frágiles expect(find.text('Total: \$42.50'), findsOneWidget); // Mejor: expect(find.textContaining('Total:'), findsOneWidget); ``` ## Debugging Integration Tests ```dart testWidgets('debug integration test', (tester) async { app.main(); await tester.pumpAndSettle(); // Imprimir widget tree debugDumpApp(); // Pausar ejecución (puede interactuar manualmente) await tester.pump(Duration(seconds: 60)); // Tomar screenshot para debugging await expectLater( find.byType(MaterialApp), matchesGoldenFile('debug/current_state.png'), ); }); ``` ## Ejecutar en CI/CD ```yaml # .github/workflows/integration_tests.yml name: Integration Tests on: [push, pull_request] jobs: test: runs-on: macos-latest steps: - uses: actions/checkout@v2 - uses: subosito/flutter-action@v2 - name: Install dependencies run: flutter pub get - name: Start iOS Simulator run: | xcrun simctl boot "iPhone 14" - name: Run integration tests run: flutter test integration_test ``` ## Conclusión Integration testing es crucial para: - ✅ **Verificar flujos completos** - ✅ **Detectar problemas de integración** - ✅ **Validar comportamiento real** - ✅ **Dar confianza antes de releases** > "Los integration tests son tu última línea de defensa antes de que el código llegue a producción" ## Pirámide de Testing ```mermaid graph BT Unit[Unit Tests: 70%
(Rápidos, Aislados)] Widget[Widget Tests: 20%
(UI, Componentes)] Integration[Integration Tests: 10%
(Lentos, End-to-End)] Unit --> Widget Widget --> Integration style Unit fill:#c8e6c9,stroke:#4caf50,stroke-width:2px style Widget fill:#fff9c4,stroke:#fbc02d,stroke-width:2px style Integration fill:#ffcdd2,stroke:#e57373,stroke-width:2px ``` **Regla 70-20-10:** - 70% Unit Tests - 20% Widget Tests - 10% Integration Tests

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.