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>()]);
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);
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
when(service.fetchUser(any)).thenReturnAsync(user);
when(service.fetchUser(any)).thenThrowAsync(Exception('fail'));
when(service.fetchData(any)).thenReturnAfter(Duration(milliseconds: 100), data);
when(service.save(any)).thenAnswerVoid();
when(service.findUser(any)).thenReturnNull();
when(service.updates()).thenEmpty();
when(service.updates()).thenEmit([a, b, c]);
when(service.updates()).thenEmitSingle(update);
when(service.updates()).thenEmitError(Exception('fail'));
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.