dart_test_extensions

pub package License: BSD-3-Clause CI

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.