Skip to content

Pruebas

Probar código que usa get_it requiere diferentes enfoques dependiendo de si estás escribiendo pruebas unitarias, pruebas de widget o pruebas de integración. Esta guía cubre mejores prácticas y patrones comunes.

Inicio Rápido: El Patrón de Scope (Recomendado)

Mejor práctica: Usa scopes para sombrear servicios reales con dobles de prueba. Esto es más limpio y mantenible que resetear get_it o usar registro condicional.

dart
setUpAll(() {
  configureDependencies(); // Register real app dependencies ONCE
});

setUp(() {
  getIt.pushNewScope(); // Create test scope
  getIt.registerSingleton<ApiClient>(MockApiClient()); // Shadow with mock
});

tearDown(() async {
  await getIt.popScope(); // Restore real services
});

test('test name', () {
  final service = getIt<UserService>();
  print('service: $service');
  // UserService automatically gets MockApiClient!
});

Beneficios clave:

  • Solo sobrescribes lo que necesitas para cada prueba
  • Limpieza automática entre pruebas
  • La misma configureDependencies() que en producción

Patrones de Pruebas Unitarias

Patrón 1: Testing Basado en Scopes (Recomendado)

Usa scopes para inyectar mocks para servicios específicos mientras mantienes el resto de tu grafo de dependencias intacto. Registrar una implementación diferente en un scope funciona de la misma manera - usando el parámetro de tipo genérico para sombrear el registro original.

dart
// Note: This example demonstrates testing patterns
// In actual tests, use flutter_test and mockito packages

class MockApiClient extends ApiClient {
  MockApiClient() : super('http://mock');

  String token = 'test-token';
  @override
  Future<Map<String, dynamic>> get(String endpoint,
      {Map<String, String>? headers}) async {
    return {'id': '123', 'name': 'Alice'};
  }
}

class MockAuthService {
  String getToken() => 'test-token';

  Future<bool> login(String username, String password) async => true;
  Future<void> logout() async {}
  Future<void> cleanup() async {}
  bool get isAuthenticated => false;
}

void main() {
  // Simplified test example showing scope-based mocking
  void setupTests() {
    // Register all real dependencies once
    getIt.registerLazySingleton<ApiClient>(() => ApiClientImpl());
    getIt.registerLazySingleton<AuthService>(() => AuthServiceImpl());
    getIt.registerLazySingleton<UserRepository>(
        () => UserRepository(getIt(), getIt()));
  }

  void runTest() async {
    // Push scope and shadow only the services we want to mock
    getIt.pushNewScope();

    final mockApi = MockApiClient();
    final mockAuth = MockAuthService();

    getIt.registerSingleton<ApiClient>(mockApi);
    getIt.registerSingleton<MockAuthService>(mockAuth);

    // UserRepository will be created fresh with our mocks
    final repo = getIt<UserRepository>();
    print('repo: $repo');
    final user = await repo.fetchUser('123');

    print('Fetched user: ${user?.name}');

    // Clean up
    await getIt.popScope();
  }

  setupTests();
  runTest();
}

Patrón 2: Registro Condicional (Alternativa)

En lugar de usar scopes, puedes cambiar implementaciones en tiempo de registro usando un flag:

dart
// Interface
abstract class PaymentProcessor {
  Future<void> processPayment(double amount);
}

// Real implementation
class StripePaymentProcessor implements PaymentProcessor {
  @override
  Future<void> processPayment(double amount) async {
    // Real Stripe API call
  }
}

// Mock implementation for testing
class MockPaymentProcessor implements PaymentProcessor {
  @override
  Future<void> processPayment(double amount) async {
    // Mock - no real API call
  }
}

// Configure with conditional registration
void configureDependencies({bool isTesting = false}) {
  if (isTesting) {
    // Register mock for testing
    getIt.registerSingleton<PaymentProcessor>(MockPaymentProcessor());
  } else {
    // Register real implementation for production
    getIt.registerSingleton<PaymentProcessor>(StripePaymentProcessor());
  }
}

// Business logic - works with either implementation!
class CheckoutService {
  Future<void> processOrder(double amount) {
    // The <PaymentProcessor> type parameter is what enables the switch
    // Without it, mock and real would register as different types
    final processor = getIt<PaymentProcessor>();
    return processor.processPayment(amount);
  }
}

Este enfoque es más simple pero menos flexible que los scopes - debes decidir qué implementación usar antes del registro, y no puedes cambiar fácilmente durante el tiempo de ejecución.

Shadowing Basado en Tipos

Cuando registras MockPaymentProcessor como <PaymentProcessor>, get_it usa el parámetro de tipo como clave de búsqueda, no la clase concreta. Esto es lo que permite cambiar implementaciones—la misma clave recupera diferentes objetos en diferentes contextos.

Patrón 3: Inyección por Constructor para Pruebas Unitarias Puras

Para probar clases en completo aislamiento (sin get_it), usa parámetros de constructor opcionales.

dart
class UserManager {
  final AppModel appModel;
  final DbService dbService;

  UserManager({
    AppModel? appModel,
    DbService? dbService,
  })  : appModel = appModel ?? getIt<AppModel>(),
        dbService = dbService ?? getIt<DbService>();

  Future<void> saveUser(User user) async {
    appModel.currentUser = user;
    await dbService.save(user);
  }
}

// In tests - no get_it needed
// test('saveUser updates model and persists to database', () async {
//   final mockModel = MockAppModel();
//   final mockDb = MockDbService();
//
//   // Create instance directly with mocks
//   final manager = UserManager(appModel: mockModel, dbService: mockDb);
//
//   await manager.saveUser(User(id: '1', name: 'Bob'));
//
//   verify(mockDb.save(any)).called(1);
// });

Cuándo usar:

  • ✅ Probar lógica de negocio pura en aislamiento
  • ✅ Clases que no necesitan el grafo completo de dependencias
  • ❌️ Pruebas estilo integración donde quieres dependencias reales

Testing de Widgets

Probando Widgets que Usan get_it

Los widgets a menudo recuperan servicios de get_it. Usa scopes para proporcionar implementaciones específicas de prueba.

dart
setUpAll(() {
  // Register app dependencies
  getIt.registerLazySingleton<ThemeService>(() => ThemeServiceImpl());
  getIt.registerLazySingleton<UserService>(
      () => UserServiceImpl(ApiClient('http://localhost')));
});

testWidgets('LoginPage displays user after successful login', (tester) async {
  // Arrange - push scope with mock
  getIt.pushNewScope();
  final mockUser = MockUserService();
  getIt.registerSingleton<UserService>(mockUser);

  // Act
  await tester.pumpWidget(const MyApp());
  await tester.enterText(find.byKey(const Key('username')), 'alice');
  await tester.enterText(find.byKey(const Key('password')), 'secret');
  await tester.tap(find.text('Login'));
  await tester.pumpAndSettle();

  // Assert
  expect(find.text('Welcome, Alice'), findsOneWidget);

  // Cleanup
  await getIt.popScope();
});

Testing con Registros Asíncronos

Si tu app usa registerSingletonAsync, asegúrate de que los servicios async estén listos antes de probar.

dart
test('widget works with async services', () async {
  getIt.pushNewScope();

  // Register async mock
  getIt.registerSingletonAsync<Database>(() async {
    await Future.delayed(Duration(milliseconds: 100));
    return MockDatabase();
  });

  // Wait for all async registrations
  await getIt.allReady();

  // Now safe to test
  final db = getIt<Database>();
  print('db: $db');
  expect(db, isA<MockDatabase>());

  await getIt.popScope();
});

Testing de Factories

Probando Registros de Factory

Las factories crean nuevas instancias en cada llamada a get() - verifica este comportamiento en las pruebas.

dart
void main() {
  test('factory creates new instance each time', () async {
    getIt.pushNewScope();

    getIt.registerFactory<ShoppingCart>(() => ShoppingCart());

    final cart1 = getIt<ShoppingCart>();
    print('cart1: $cart1');
    final cart2 = getIt<ShoppingCart>();
    print('cart2: $cart2');

    expect(identical(cart1, cart2), false); // Different instances

    await getIt.popScope();
  });
}

Probando Factories Parametrizadas

dart
test('factory param passes parameters correctly', () async {
  getIt.pushNewScope();

  getIt.registerFactoryParam<UserViewModel, String, void>(
    (userId, _) => UserViewModel(userId),
  );

  final vm = getIt<UserViewModel>(param1: 'user-123');
  print('vm: $vm');
  expect(vm.userId, 'user-123');

  await getIt.popScope();
});

Escenarios Comunes de Testing

Escenario 1: Probando Servicio con Múltiples Dependencias
dart
// Service under test - uses get_it directly when accessing dependencies
class SyncService {
  Future<void> syncData() async {
    if (!getIt<AuthService>().isAuthenticated) return;
    final data = await getIt<ApiClient>().fetchData();
    await getIt<Database>().save(data);
  }
}

void main() {
  test('SyncService uses mocked dependencies', () async {
    getIt.pushNewScope();

    // Register mocks - SyncService will get these via getIt<Type>()
    getIt.registerSingleton<AuthService>(
        MockAuthService()..isAuthenticated = true);
    getIt.registerSingleton<ApiClient>(
        MockApiClient()..mockData = {'data': 'value'});
    getIt.registerSingleton<Database>(MockDatabase());

    // Service under test uses get_it to access mocks
    final sync = SyncService();
    await sync.syncData();

    // Test assertions would go here...

    await getIt.popScope();
  });
Escenario 2: Probando Servicios con Scope
dart
test('service lifecycle matches scope lifecycle', () async {
  // Base scope
  getIt.registerLazySingleton<CoreService>(() => CoreService());

  // Feature scope
  getIt.pushNewScope(scopeName: 'feature');
  getIt.registerLazySingleton<FeatureService>(() => FeatureService(getIt()));

  expect(getIt<CoreService>(), isNotNull);
  expect(getIt<FeatureService>(), isNotNull);

  // Pop feature scope
  await getIt.popScope();

  expect(getIt<CoreService>(), isNotNull); // Still available
  expect(() => getIt<FeatureService>(), throwsStateError); // Gone!
});
Escenario 3: Probando Disposal
dart
class DisposableService implements Disposable {
  bool disposed = false;

  @override
  FutureOr onDispose() {
    disposed = true;
  }
}

Mejores Prácticas

✅ Haz

  1. Usa scopes para aislamiento de pruebas

    dart
    setUp(() => getIt.pushNewScope());
       tearDown(() async => await getIt.popScope());
  2. Registra dependencias reales una vez en setUpAll()

    dart
    setUpAll(() {
         configureDependencies(); // Same as production
       });
  3. Sombrea solo lo que necesitas simular

    dart
    setUp(() {
         getIt.pushNewScope();
         getIt.registerSingleton<ApiClient>(MockApiClient()); // Only mock this
         // Everything else uses real registrations from base scope
       });
  4. Haz await de popScope() si los servicios tienen disposal async

    dart
    tearDown(() async {
    
    Future<void> main() async {
         await getIt.popScope(); // Ensures cleanup completes
       });
    }
  5. Usa allReady() para registros async

    dart
    Future<void> main() async {
      await getIt.allReady(); // Wait before testing
    }

❌️ No Hagas

  1. No llames a reset() entre pruebas

    dart
    Future<void> main() async {
      // ❌ Bad - loses all registrations
      tearDown(() async {
        await getIt.reset();
      });
    
      // ✅ Good - use scopes instead
      tearDown(() async {
        await getIt.popScope();
      });
    }
  2. No re-registres todo en cada prueba

    dart
    // ❌ Bad - duplicates production setup
       setUp(() {
         getIt.registerLazySingleton<ApiClient>(...);
         getIt.registerLazySingleton<Database>(...);
         // ... 50 more registrations
       });
    
       // ✅ Good - reuse production setup
       setUpAll(() {
         configureDependencies(); // Call once
       });
  3. No uses allowReassignment en pruebas

    dart
    // ❌ Bad - masks bugs
       getIt.allowReassignment = true;
    
       // ✅ Good - use scopes for isolation
  4. No olvides hacer pop de scopes en tearDown

    dart
    // ❌ Bad - scopes leak into next test
       test('...', () {
         getIt.pushNewScope();
         // ... test code
         // Missing popScope()!
       });

Solución de Problemas

"Object/factory already registered" en pruebas

Causa: El scope no se hizo pop en la prueba anterior, o no se esperó a reset().

Solución:

dart
tearDown(() async {

Future<void> main() async {
  await getIt.popScope(); // Always await!
  });
}

Los Mocks no se están usando

Causa: El mock fue registrado en el scope equivocado o después de que el servicio ya fue creado.

Solución: Empuja el scope y registra mocks antes de acceder a los servicios:

dart
setUp(() {
  getIt.pushNewScope();
  getIt.registerSingleton<ApiClient>(mockApi); // Register FIRST
});

test('test name', () {
  final service = getIt<UserService>(); // Accesses AFTER mock registered
  // ...
});

El servicio async no está listo

Causa: Intentando acceder a registro async antes de que se complete.

Solución:

dart
test('async test', () async {

Future<void> main() async {
  await getIt.allReady(); // Wait for all async registrations
  final db = getIt<Database>();
  // ...
  });
}

Ver También

Publicado bajo la Licencia MIT.