Testing Events
Assert that your modules, cubits, and services fire the right events at the right time.
Overview
The testing API provides two ways to test events:
ModularTestScope.fireEvent+eventsOf— For integration-style tests where the scope manages the full lifecycle.EventRecorder— Standalone recorder for tests that don't need the full scope.
Both rely on ModularEventBus.fire under the hood, which dispatches to the global defaultModularEventBus.
Using ModularTestScope for Events
Setup
Call listenFor<E>() after scope.setUp() to start recording a specific event type:
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router_modular/testing.dart';
void main() {
final scope = ModularTestScope.fresh();
setUp(() {
scope.setUp();
scope.listenFor<PaymentProcessedEvent>();
});
tearDown(scope.tearDown);
test('fires PaymentProcessedEvent on successful charge', () async {
scope.fireEvent(PaymentProcessedEvent(amount: 100));
await Future.delayed(Duration(milliseconds: 50));
final events = scope.eventsOf<PaymentProcessedEvent>();
expect(events.length, equals(1));
expect(events[0].amount, equals(100));
});
}Multiple Event Types
Call listenFor once per type you want to track:
setUp(() {
scope.setUp();
scope.listenFor<OrderPlacedEvent>();
scope.listenFor<PaymentProcessedEvent>();
});
test('fires both events on checkout', () async {
scope.fireEvent(OrderPlacedEvent(orderId: 42));
scope.fireEvent(PaymentProcessedEvent(amount: 500));
await Future.delayed(Duration(milliseconds: 50));
expect(scope.eventsOf<OrderPlacedEvent>().length, equals(1));
expect(scope.eventsOf<PaymentProcessedEvent>().length, equals(1));
});Clearing Between Assertions
Use clearRecordedEvents() to reset recorded events without stopping the listener:
test('records events independently per step', () async {
scope.fireEvent(PaymentProcessedEvent(amount: 100));
await Future.delayed(Duration(milliseconds: 50));
expect(scope.eventsOf<PaymentProcessedEvent>().length, equals(1));
scope.clearRecordedEvents();
scope.fireEvent(PaymentProcessedEvent(amount: 200));
await Future.delayed(Duration(milliseconds: 50));
expect(scope.eventsOf<PaymentProcessedEvent>().length, equals(1));
expect(scope.eventsOf<PaymentProcessedEvent>()[0].amount, equals(200));
});Using EventRecorder Standalone
EventRecorder is useful when you need finer control or when your test doesn't use a full ModularTestScope:
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router_modular/testing.dart';
void main() {
late EventRecorder recorder;
setUp(() {
recorder = EventRecorder.fresh();
recorder.listenFor<NotificationSentEvent>();
});
tearDown(() {
recorder.dispose();
});
test('service fires NotificationSentEvent', () async {
final service = NotificationService();
service.send('Hello');
await Future.delayed(Duration(milliseconds: 50));
final events = recorder.eventsOf<NotificationSentEvent>();
expect(events.length, equals(1));
expect(events[0].message, equals('Hello'));
});
}EventRecorder API
| Method | Description |
|---|---|
EventRecorder.fresh() | Creates a new recorder with no active listeners |
listenFor<E>({EventBus? eventBus}) | Starts recording events of type E |
eventsOf<E>() | Returns a RecordedEventList<E> snapshot |
clear() | Clears recorded events without stopping listeners |
dispose() | Cancels all listeners and clears state |
eventsOf<E>()returns an empty list iflistenFor<E>()was never called — no exception.
RecordedEventList
RecordedEventList<E> is the return type of eventsOf. It wraps the captured events in a first-class collection:
final events = scope.eventsOf<PaymentProcessedEvent>();
events.length // number of captured events
events.isEmpty // true if none were captured
events.isNotEmpty // true if at least one was captured
events[0] // access by index
events.first // first event
events.last // last event
events.any((e) => e.amount > 100) // predicate check
events.where((e) => e.amount > 100) // returns new RecordedEventList<E>
events.toList() // unmodifiable List<E>Asserting Event Content
test('payment amount is correct', () async {
scope.fireEvent(PaymentProcessedEvent(amount: 250));
await Future.delayed(Duration(milliseconds: 50));
final events = scope.eventsOf<PaymentProcessedEvent>();
expect(events, isNotEmpty);
expect(events.first.amount, equals(250));
expect(events.any((e) => e.amount == 250), isTrue);
});Filtering Events
test('only high-value payments are flagged', () async {
scope.fireEvent(PaymentProcessedEvent(amount: 50));
scope.fireEvent(PaymentProcessedEvent(amount: 500));
scope.fireEvent(PaymentProcessedEvent(amount: 1500));
await Future.delayed(Duration(milliseconds: 50));
final highValue = scope
.eventsOf<PaymentProcessedEvent>()
.where((e) => e.amount >= 500);
expect(highValue.length, equals(2));
});Testing EventModule
You can test an EventModule directly by calling initState with a FakeInjector and then firing events:
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router_modular/testing.dart';
class AuthModule extends EventModule {
final List<String> navigatedTo = [];
@override
void listen() {
on<UserLoggedInEvent>((event, context) {
navigatedTo.add('/dashboard');
});
}
@override
List<ModularRoute> get routes => [
ChildRoute('/login', child: (_, __) => Container()),
];
}
void main() {
setUp(clearEventModuleState);
test('AuthModule navigates on UserLoggedInEvent', () async {
final module = AuthModule();
module.initState(FakeInjector.empty());
ModularEventBus.fire(UserLoggedInEvent(userId: '42'));
await Future.delayed(Duration(milliseconds: 50));
expect(module.navigatedTo, contains('/dashboard'));
module.dispose();
});
}Testing ModularEventMixin in Widgets
For widgets that use ModularEventMixin, use widget tests with ModularTestScope:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router_modular/testing.dart';
import 'package:go_router_modular/go_router_modular.dart';
class CounterWidget extends StatefulWidget {
const CounterWidget({super.key});
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget>
with ModularEventMixin {
int count = 0;
@override
void initState() {
super.initState();
on<IncrementEvent>((event, _) {
setState(() => count += event.amount);
});
}
@override
Widget build(BuildContext context) => Text('$count');
}
void main() {
setUp(clearEventModuleState);
testWidgets('counter increments on IncrementEvent', (tester) async {
await tester.pumpWidget(
MaterialApp(home: CounterWidget()),
);
ModularEventBus.fire(IncrementEvent(amount: 5));
await tester.pump(Duration(milliseconds: 50));
expect(find.text('5'), findsOneWidget);
});
}ModularEventMixin cancels all subscriptions automatically when the widget is disposed — no teardown needed.
Custom EventBus
To isolate tests from the global bus, pass a custom EventBus:
final customBus = EventBus();
recorder.listenFor<MyEvent>(eventBus: customBus);
customBus.fire(MyEvent()); // only this recorder captures itThis is useful when testing modules configured with their own EventBus.