mogen_unit_tests 1.1.4
mogen_unit_tests: ^1.1.4 copied to clipboard
Scans Riverpod AsyncNotifier classes and auto-generates Mocktail unit tests.
mogen_unit_tests #
mogen_unit_tests scans your Flutter feature folders, parses Riverpod notifiers, and generates Mocktail-based unit test scaffolding for each discovered notifier.
It is designed for projects that keep their notifier and state files under lib/features/**/presentation/ and want a fast starting point for repeatable unit tests.
What it generates #
For every notifier it finds, the package creates a ready-to-run test file with:
Mock<Type>classes for every dependency reached viaref.read(...)/ref.watch(...)— repositories, services, clients, and other notifiers alike- a
ProviderContainerwith dependency overrides insetUp - any dependency calls made from
build()stubbed once at the bottom ofsetUp(after the overrides), so initializing the notifier never throwsMissingStubError— and so those stubs aren't repeated identically inside every method's test container.dispose()intearDown- two tests per public notifier method: one exercising the success path, one where the dependency throws
AppException.test()to exercise the error path ClassName.empty()used for custom-type parameters and stub return values (noFakeclasses, noregisterFallbackValue)- optional state field assertions when
stateInfois available
Conventions this generator assumes #
The generated tests lean on a few conventions rather than guessing blindly. If your project doesn't follow them, the generated file will need manual edits to compile:
- Errors:
lib/core/errors/app_exception.dartdefinesAppExceptionwith afactory AppException.test()constructor — this is what error-path stubs throw. - Repositories: interfaces live at
lib/features/<feature>/domain/repositories/<name>.dart, with an implementation atlib/features/<feature>/data/repositories/<name>_impl.dart. A dependency doesn't have to belong to the consuming notifier's own feature — the generator scans the whole project to find where it actually lives. - Entities/models: any custom type used as a parameter or stub return value exposes a
ClassName.empty()factory constructor. - Router: a
routerProviderdependency is assumed to exposego_router'sGoRouter, not Flutter's ownRouterclass.
Project layout #
The CLI expects this layout inside your Flutter app:
lib/
└── features/
└── cart/
└── presentation/
├── notifiers/
│ └── cart_notifier.dart
└── states/
└── cart_state.dart
Installation #
Add the package to your Flutter app's dev_dependencies:
dev_dependencies:
mogen_unit_tests:
Install it:
dart pub get
Usage #
Run the generator from the root of your Flutter app:
dart run mogen_unit_tests
CLI options #
| Flag | Short | Default | Description |
|---|---|---|---|
--root |
-r |
. |
Project root directory |
--dry-run |
-d |
false |
Preview generated files without writing them |
--verbose |
-v |
false |
Print progress details while scanning and generating |
--version |
Print the package version and exit | ||
--help |
-h |
Show the CLI help text |
Examples #
# Generate tests from the current directory
dart run mogen_unit_tests
# Preview the output without writing files
dart run mogen_unit_tests --dry-run --verbose
# Run against a different Flutter project
dart run mogen_unit_tests --root /path/to/my_flutter_app
Example #
Input notifier #
class CartNotifier extends AsyncNotifier<CartState> {
@override
Future<CartState> build() async {
return ref.read(cartRepositoryProvider).fetchCart();
}
Future<void> addItem(CartItem item) async {
await ref.read(cartRepositoryProvider).addItem(item);
}
Future<void> removeItem(String itemId) async {
await ref.read(cartRepositoryProvider).removeItem(itemId);
}
}
Input repository interface #
abstract class CartRepository {
Future<CartEntity> fetchCart();
Future<void> addItem(CartItem item);
Future<void> removeItem(String itemId);
}
Input state #
class CartState {
final bool isLoadingAction;
final String? error;
final String? success;
const CartState({this.isLoadingAction = false, this.error, this.success});
}
Generated output #
Note: the generator creates test groups for public notifier methods only — build() is not scaffolded as its own test group, but any dependency call it makes is stubbed once in setUp so initialization doesn't throw.
// GENERATED BY mogen_unit_tests — YOU CAN REMOVE THIS COMMENT
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:my_app/core/errors/app_exception.dart';
import 'package:my_app/features/cart/presentation/notifiers/cart_notifier.dart';
import 'package:my_app/features/cart/presentation/states/cart_state.dart';
import 'package:my_app/features/cart/domain/repositories/cart_repository.dart';
import 'package:my_app/features/cart/data/repositories/cart_repository_impl.dart';
import 'package:my_app/features/cart/domain/entities/cart_entity.dart';
// ── Mocks ────────────────────────────────────────────────────
class MockCartRepository extends Mock implements CartRepository {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('CartNotifier', () {
late MockCartRepository mockCartRepository;
late ProviderContainer container;
setUp(() {
mockCartRepository = MockCartRepository();
container = ProviderContainer(
overrides: [
// Override the provider so the notifier gets mockCartRepository
cartRepositoryProvider.overrideWithValue(mockCartRepository),
],
);
// Arrange: stub dependencies used by build()
when(
() => mockCartRepository.fetchCart(),
).thenAnswer((_) async => CartEntity.empty());
});
tearDown(() {
container.dispose();
});
// ── addItem() ──────────────────────────────────────
group('addItem', () {
test('addItem completes successfully', () async {
// Arrange: stub repositories
when(
() => mockCartRepository.addItem(any()),
).thenAnswer((_) async => null);
// Arrange: inputs
final item = CartItem.empty();
// Ensure notifier is initialised
await container.read(cartNotifierProvider.future);
// Act
await container.read(cartNotifierProvider.notifier).addItem(item);
// Assert
final finalState = container.read(cartNotifierProvider);
expect(finalState.requireValue.isLoadingAction, isFalse);
expect(finalState.requireValue.error, isNull);
expect(finalState.requireValue.success, isNotNull);
});
test('addItem shows an error when the repository fails', () async {
// Arrange: stub repositories
when(
() => mockCartRepository.addItem(any()),
).thenThrow(AppException.test());
// Arrange: inputs
final item = CartItem.empty();
// Ensure notifier is initialised
await container.read(cartNotifierProvider.future);
// Act
await container.read(cartNotifierProvider.notifier).addItem(item);
// Assert
final finalState = container.read(cartNotifierProvider);
expect(finalState.requireValue.isLoadingAction, isFalse);
expect(finalState.requireValue.error, isNotNull);
expect(finalState.requireValue.success, isNull);
});
});
// ── removeItem() ──────────────────────────────────────
group('removeItem', () {
test('removeItem completes successfully', () async {
// Arrange: stub repositories
when(
() => mockCartRepository.removeItem(any()),
).thenAnswer((_) async => null);
// Arrange: inputs
const itemId = '';
// Ensure notifier is initialised
await container.read(cartNotifierProvider.future);
// Act
await container.read(cartNotifierProvider.notifier).removeItem(itemId);
// Assert
final finalState = container.read(cartNotifierProvider);
expect(finalState.requireValue.isLoadingAction, isFalse);
expect(finalState.requireValue.error, isNull);
expect(finalState.requireValue.success, isNotNull);
});
test('removeItem shows an error when the repository fails', () async {
// Arrange: stub repositories
when(
() => mockCartRepository.removeItem(any()),
).thenThrow(AppException.test());
// Arrange: inputs
const itemId = '';
// Ensure notifier is initialised
await container.read(cartNotifierProvider.future);
// Act
await container.read(cartNotifierProvider.notifier).removeItem(itemId);
// Assert
final finalState = container.read(cartNotifierProvider);
expect(finalState.requireValue.isLoadingAction, isFalse);
expect(finalState.requireValue.error, isNotNull);
expect(finalState.requireValue.success, isNull);
});
});
});
}
What you still need to fill in #
The generator creates dependency method stubs automatically. If a method returns a non-null value, update the generated thenAnswer(...) call to return a realistic object instead of the auto-generated ClassName.empty()/literal placeholder.
Everything else is generated for you.
Supported notifier types #
| Class | Supported |
|---|---|
AsyncNotifier<T> |
✅ |
AutoDisposeAsyncNotifier<T> |
✅ |
Notifier<T> |
✅ |
AutoDisposeNotifier<T> |
✅ |
Dependencies of any kind — repositories, services, clients, other notifiers — are detected from ref.read(...) and ref.watch(...) calls inside the notifier implementation.
Limitations #
- Generates unit tests only — no widget or integration tests
- Dependency stubs are autogenerated and may require manual return-value adjustments
- Only notifiers under
presentation/notifiers/are scanned familynotifiers are not supported yet- Assumes
AppException.test()exists (see Conventions this generator assumes) — projects without it will need to adjust the error-path stubs manually
Dependencies #
| Package | Purpose |
|---|---|
analyzer |
AST parsing of notifier source files |
dart_style |
Formatting generated Dart output |
args |
CLI flag parsing |
glob |
Discovering feature folders |
path |
Cross-platform path handling |
yaml |
Reading package name from pubspec.yaml |
Project health notes #
- This package is a CLI tool, not a Flutter app. Running
dart run mogen_unit_testsfrom this repository itself will not produce output unless you point it at a consuming Flutter app withlib/features/.... - The current output destination is
test/unit/features/..., which is now reflected in the examples above.