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.
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.
// 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:
// 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.
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.
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.
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.
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
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
// 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
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
class DisposableService implements Disposable {
bool disposed = false;
@override
FutureOr onDispose() {
disposed = true;
}
}Mejores Prácticas
✅ Haz
Usa scopes para aislamiento de pruebas
dartsetUp(() => getIt.pushNewScope()); tearDown(() async => await getIt.popScope());Registra dependencias reales una vez en
setUpAll()dartsetUpAll(() { configureDependencies(); // Same as production });Sombrea solo lo que necesitas simular
dartsetUp(() { getIt.pushNewScope(); getIt.registerSingleton<ApiClient>(MockApiClient()); // Only mock this // Everything else uses real registrations from base scope });Haz await de
popScope()si los servicios tienen disposal asyncdarttearDown(() async { Future<void> main() async { await getIt.popScope(); // Ensures cleanup completes }); }Usa
allReady()para registros asyncdartFuture<void> main() async { await getIt.allReady(); // Wait before testing }
❌️ No Hagas
No llames a
reset()entre pruebasdartFuture<void> main() async { // ❌ Bad - loses all registrations tearDown(() async { await getIt.reset(); }); // ✅ Good - use scopes instead tearDown(() async { await getIt.popScope(); }); }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 });No uses
allowReassignmenten pruebasdart// ❌ Bad - masks bugs getIt.allowReassignment = true; // ✅ Good - use scopes for isolationNo 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:
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:
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:
test('async test', () async {
Future<void> main() async {
await getIt.allReady(); // Wait for all async registrations
final db = getIt<Database>();
// ...
});
}Ver También
- Scopes - Documentación detallada de scopes
- Registro de Objetos - Tipos de registro