dart_test_extensions
Dart test extensions for cleaner, more expressive tests — ChangeNotifier state capture, mockito sugar, stream helpers, and widget test utilities.
Installation
Add as a dev dependency (you only use this in tests):
flutter pub add --dev dart_test_extensions
Imports
// Core: ChangeNotifier sugar, stream helpers, widget test helpers
import 'package:dart_test_extensions/dart_test_extensions.dart';
// Mockito PostExpectation + VerificationResult sugar
import 'package:dart_test_extensions/mockito.dart';
Features
ChangeNotifier / ViewModel Testing
capture — Record values on each notification:
// Before
final states = <MyState>[];
viewModel.addListener(() => states.add(viewModel.state));
await viewModel.onLoad();
expect(states.first, isA<Loading>());
expect(states.last, isA<Loaded>());
// After
final states = viewModel.capture((vm) => vm.state);
await viewModel.onLoad();
states.shouldTransition([isA<Loading>(), isA<Loaded>()]);
Individual access and exact-value assertions:
final states = viewModel.capture((vm) => vm.state);
await viewModel.onLoad();
expect(states.first, isA<Loading>());
expect(states[1], isA<Loaded>());
states.shouldEqual([Loading(), Loaded(['a', 'b'])]);
count — Count notifications:
// Before
var notifyCount = 0;
viewModel.addListener(() => notifyCount++);
await viewModel.onLoginPressed(email: email, password: password);
expect(notifyCount, 2);
// After
final counter = viewModel.count();
await viewModel.onLoginPressed(email: email, password: password);
counter.shouldBe(2);
counter.shouldBeAtLeast(1);
counter.reset(); // reset to 0 for next action
trackProperty — Track a sub-property through state changes:
final loadingMore = viewModel.trackProperty<ConversationsState, bool>(
(vm) => vm.state,
(state) => state.isLoadingMore,
when: (state) => state is ConversationsLoaded,
);
await viewModel.onLoadMore();
expect(loadingMore, [true, false]);
Mockito Sugar
Future mocks:
// Before → After
when(service.fetchUser(any)).thenAnswer((_) async => user);
when(service.fetchUser(any)).thenReturnAsync(user);
when(service.fetchUser(any)).thenAnswer((_) => Future.error(Exception('fail')));
when(service.fetchUser(any)).thenThrowAsync(Exception('fail'));
when(service.fetchData(any)).thenAnswer((_) async {
await Future.delayed(const Duration(milliseconds: 100));
return data;
});
when(service.fetchData(any)).thenReturnAfter(Duration(milliseconds: 100), data);
when(service.save(any)).thenAnswer((_) => Future<void>.value());
when(service.save(any)).thenAnswerVoid();
when(service.findUser(any)).thenAnswer((_) async => null);
when(service.findUser(any)).thenReturnNull();
Stream mocks:
when(service.updates()).thenEmpty();
when(service.updates()).thenEmit([a, b, c]);
when(service.updates()).thenEmitSingle(update);
when(service.updates()).thenEmitError(Exception('fail'));
Typed captures:
// Before
final captured = verify(router.pushTo(captureAny)).captured.single as MyRoute;
// After
final route = verify(router.pushTo(captureAny)).capturedAs<MyRoute>();
final logs = verify(logger.log(captureAny)).allCapturedAs<String>();
Stream Helpers
// Emit and wait for microtask drain
await playbackController.emitAndSettle(PlaybackState.playing);
// Collect all stream events during an action
final effects = await viewModel.effects.collectDuring(() async {
await viewModel.onSave();
});
expect(effects, [MyEffect.saved]);
Widget Test Helpers
// Pump with MaterialApp wrapper
await tester.pumpApp(const MyWidget(), theme: myTheme);
await tester.pumpScaffold(const MyWidget(), theme: myTheme);
// Tap helpers
await tester.tapAndPump(find.byType(ElevatedButton));
await tester.tapAndSettle(find.byType(ElevatedButton));
// Finder assertions
find.text('Hello').shouldExist();
find.text('Error').shouldNotExist();
find.byType(ListTile).shouldFind(3);
Contributing
Contributions are welcome. Please open an issue first to discuss what you would like to change.
License
BSD-3-Clause — see LICENSE for details.
Libraries
- dart_test_extensions
- Dart test extensions for cleaner, more expressive tests.
- mockito
- Mockito sugar extensions for stubbing futures, streams, and verifications.