Testing
Unit Testing

Unit Testing

Write fast, isolated unit tests for services, use cases, and modules without touching the global DI container.

Overview

FakeInjector is an immutable, in-memory implementation of InjectorReader. It allows you to construct objects with their dependencies resolved from a plain map — no Bind.register, no global state, no setUp required.

Use FakeInjector when you want to test a single class in isolation. Use ModularTestScope when you need the full module ecosystem.

FakeInjector

Building an Injector

Start with FakeInjector.empty() and chain add<T>() calls. Each add returns a new injector — the original is never modified:

order_service_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router_modular/testing.dart';
 
void main() {
  test('OrderService processes payment', () {
    final gateway = FakePaymentGateway();
    final repo = FakeUserRepository();
 
    final injector = FakeInjector.empty()
        .add<PaymentGateway>(gateway)
        .add<UserRepository>(repo);
 
    final service = OrderService(
      injector.get<PaymentGateway>(),
      injector.get<UserRepository>(),
    );
 
    final result = service.processOrder(userId: 1, amount: 100);
 
    expect(result, isTrue);
    expect(gateway.lastCharged, equals(100));
  });
}

Resolving Dependencies

injector.get<PaymentGateway>()   // returns the registered instance
injector.get<DateTime>()         // throws FakeInjectorMissingBindError

If a type is not registered, get throws FakeInjectorMissingBindError with a descriptive message:

FakeInjectorMissingBindError: nenhum bind registrado para o tipo DateTime.
Use FakeInjector.empty().add<DateTime>(instance) para registrá-lo.

Immutability

add never modifies the receiver. This prevents accidental sharing between tests:

final base = FakeInjector.empty().add<String>('shared');
 
// Each test gets its own injector without affecting 'base'
final forTest1 = base.add<int>(1);
final forTest2 = base.add<int>(2);

FakeInjector API

MethodDescription
FakeInjector.empty()Creates an injector with no registrations
add<T>(instance)Returns a new injector with Tinstance added
get<T>({String? key})Returns the instance for T or throws FakeInjectorMissingBindError

Testing Services

Service with Multiple Dependencies

checkout_service_test.dart
class CheckoutService {
  final PaymentGateway _gateway;
  final InventoryRepository _inventory;
  final OrderRepository _orders;
 
  CheckoutService(this._gateway, this._inventory, this._orders);
 
  bool checkout(int productId, int userId) {
    if (!_inventory.isAvailable(productId)) return false;
    _orders.create(productId, userId);
    return _gateway.charge(productId);
  }
}
 
void main() {
  FakeInjector buildInjector({
    PaymentGateway? gateway,
    InventoryRepository? inventory,
    OrderRepository? orders,
  }) {
    return FakeInjector.empty()
        .add<PaymentGateway>(gateway ?? FakePaymentGateway())
        .add<InventoryRepository>(inventory ?? FakeInventoryRepository())
        .add<OrderRepository>(orders ?? FakeOrderRepository());
  }
 
  test('checkout succeeds when product is available', () {
    final injector = buildInjector();
    final service = CheckoutService(
      injector.get<PaymentGateway>(),
      injector.get<InventoryRepository>(),
      injector.get<OrderRepository>(),
    );
 
    expect(service.checkout(productId: 1, userId: 1), isTrue);
  });
 
  test('checkout fails when product is out of stock', () {
    final injector = buildInjector(
      inventory: AlwaysUnavailableInventory(),
    );
    final service = CheckoutService(
      injector.get<PaymentGateway>(),
      injector.get<InventoryRepository>(),
      injector.get<OrderRepository>(),
    );
 
    expect(service.checkout(productId: 1, userId: 1), isFalse);
  });
}

Testing EventModule with FakeInjector

Pass a FakeInjector to module.initState() to initialize a module without the full routing setup:

analytics_module_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router_modular/testing.dart';
 
class AnalyticsModule extends EventModule {
  final List<String> trackedEvents = [];
 
  @override
  void listen() {
    on<PageViewedEvent>((event, _) {
      trackedEvents.add(event.pageName);
    });
  }
 
  @override
  List<ModularRoute> get routes => [
    ChildRoute('/', child: (_, __) => Container()),
  ];
}
 
void main() {
  setUp(clearEventModuleState);
 
  test('tracks page views', () async {
    final module = AnalyticsModule();
 
    // Initialize with an empty FakeInjector — no DI dependencies needed
    module.initState(FakeInjector.empty());
 
    ModularEventBus.fire(PageViewedEvent(pageName: 'HomeScreen'));
    await Future.delayed(Duration(milliseconds: 50));
 
    expect(module.trackedEvents, contains('HomeScreen'));
 
    module.dispose();
  });
 
  test('stops tracking after dispose', () async {
    final module = AnalyticsModule();
    module.initState(FakeInjector.empty());
    module.dispose();
 
    ModularEventBus.fire(PageViewedEvent(pageName: 'ProfileScreen'));
    await Future.delayed(Duration(milliseconds: 50));
 
    expect(module.trackedEvents, isEmpty);
  });
}

When to Use What

ScenarioRecommended tool
Testing a service or use case in isolationFakeInjector
Testing DI wiring (module registers correct types)ModularTestScope
Testing event handling in an EventModuleFakeInjector + ModularEventBus.fire
Testing event handling in a widgetclearEventModuleState + ModularEventBus.fire
Integration tests with full module lifecycleModularTestScope

Best Practices

  1. Prefer FakeInjector for unit tests — It has no side effects, runs synchronously, and requires no cleanup.
  2. Build a factory helper — A buildInjector({...}) function with optional overrides keeps tests DRY.
  3. Dispose modules after testing — Always call module.dispose() in tearDown or at the end of the test to cancel event subscriptions.
  4. Call clearEventModuleState in setUp — Prevents subscriptions from a previous test from leaking into the next one when using EventModule directly.
  5. Use ModularTestScope when testing DI resolution — It ensures the global container is clean before each test.