mogen_unit_tests 1.0.3
mogen_unit_tests: ^1.0.3 copied to clipboard
CLI tool that scans lib/features/**/presentation/notifiers/, parses Riverpod AsyncNotifier / Notifier classes, detects repository dependencies, and generates Mocktail-based unit tests with success + e [...]
mogen_unit_tests #
A CLI tool that scans your Flutter feature folders, reads your Riverpod AsyncNotifier / Notifier classes, detects repository dependencies, and auto-generates Mocktail-based unit tests with success and error cases for every method.
What it generates #
For every notifier it finds, the tool produces a ready-to-run test file with:
Mock<Repository>classes for every detected dependencyFake<Type>classes +registerFallbackValuefor every complex parameter type- A
ProviderContainerwith repository provider overrides insetUp container.dispose()intearDown- A
build()group with a success test and an error test - A group per public method, each with a success test and an error test
Expected folder structure #
The tool expects this layout inside your Flutter project:
lib/
└── features/
└── cart/ ← feature name (any name)
└── presentation/
├── notifiers/
│ └── cart_notifier.dart ← your AsyncNotifier lives here
└── states/
└── cart_state.dart ← optional, used to infer field types
Generated tests are written to:
test/
└── unit/
└── features/
└── cart/
└── cart_notifier_test.dart
Installation #
Add to your Flutter project's pubspec.yaml under dev_dependencies:
dev_dependencies:
mogen_unit_tests: ^1.0.3
Then run:
dart pub get
Usage #
From the root of your Flutter project:
dart run mogen_unit_tests
Options #
| Flag | Short | Default | Description |
|---|---|---|---|
--root |
-r |
. |
Project root directory |
--dry-run |
-d |
false |
Preview what would be generated without writing files |
--verbose |
-v |
false |
Print detailed progress |
--version |
Print version and exit | ||
--help |
-h |
Show help |
Examples #
# Normal run from project root
dart run mogen_unit_tests
# Preview without writing any files
dart run mogen_unit_tests --dry-run --verbose
# Run from a different directory
dart run mogen_unit_tests --root /path/to/my_project
Example input and output #
Your notifier (lib/features/cart/presentation/notifiers/cart_notifier.dart) #
@riverpod
class CartNotifier extends _$CartNotifier {
@override
Future<CartState> build() async {
final repo = ref.read(cartRepositoryProvider);
return repo.fetchCart();
}
Future<void> addItem(CartItem item) async {
final repo = ref.read(cartRepositoryProvider);
await repo.addItem(item);
ref.invalidateSelf();
}
Future<void> clearCart() async {
final repo = ref.read(cartRepositoryProvider);
await repo.clearCart();
ref.invalidateSelf();
}
}
Your state (lib/features/cart/presentation/states/cart_state.dart) #
class CartState {
final List<CartItem> items;
final double total;
const CartState({required this.items, required this.total});
}
Generated test (test/features/cart/presentation/notifiers/cart_notifier_test.dart) #
// GENERATED BY mogen_unit_tests — DO NOT EDIT
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:my_app/features/cart/presentation/notifiers/cart_notifier.dart';
import 'package:my_app/features/cart/presentation/states/cart_state.dart';
// TODO: import the file that declares CartRepository
// ── Mocks ─────────────────────────────────────────────────────────────────
class MockCartRepository extends Mock implements CartRepository {}
// ── Fakes (registerFallbackValue) ─────────────────────────────────────────
class FakeCartItem extends Fake implements CartItem {}
void main() {
group('CartNotifier', () {
late MockCartRepository mockCartRepository;
late ProviderContainer container;
setUpAll(() {
registerFallbackValue(FakeCartItem());
});
setUp(() {
mockCartRepository = MockCartRepository();
container = ProviderContainer(
overrides: [
cartRepositoryProvider.overrideWithValue(mockCartRepository),
],
);
});
tearDown(() {
container.dispose();
});
// ── build() ─────────────────────────────────────────────────────────
group('build', () {
test('returns CartState successfully', () async {
// Arrange: stub repositories
// when(() => mockCartRepository.someMethod(any()))
// .thenAnswer((_) async => CartState(...));
// Act
final result = await container.read(cartNotifierProvider.future);
// Assert
expect(result, isA<CartState>());
});
test('emits error when repository throws', () async {
// when(() => mockCartRepository.someMethod(any()))
// .thenThrow(Exception('test error'));
await container
.read(cartNotifierProvider.future)
.catchError((_) => FakeCartState());
expect(container.read(cartNotifierProvider), isA<AsyncError>());
});
});
// ── addItem() ────────────────────────────────────────────────────────
group('addItem', () {
test('addItem completes successfully', () async {
// Arrange: inputs
final item = FakeCartItem();
await container.read(cartNotifierProvider.future);
// Act
await container.read(cartNotifierProvider.notifier).addItem(item);
// Assert
// verify(() => mockCartRepository.someMethod(any())).called(1);
});
test('addItem handles error from repository', () async {
// when(() => mockCartRepository.someMethod(any()))
// .thenThrow(Exception('test error'));
final item = FakeCartItem();
await container.read(cartNotifierProvider.future).catchError((_) {});
expect(
() async => container.read(cartNotifierProvider.notifier).addItem(item),
throwsA(isA<Exception>()),
);
});
});
// ── clearCart() ──────────────────────────────────────────────────────
group('clearCart', () {
test('clearCart completes successfully', () async {
await container.read(cartNotifierProvider.future);
await container.read(cartNotifierProvider.notifier).clearCart();
// verify(() => mockCartRepository.someMethod(any())).called(1);
});
test('clearCart handles error from repository', () async {
// when(() => mockCartRepository.someMethod(any()))
// .thenThrow(Exception('test error'));
await container.read(cartNotifierProvider.future).catchError((_) {});
expect(
() async => container.read(cartNotifierProvider.notifier).clearCart(),
throwsA(isA<Exception>()),
);
});
});
});
}
After generation #
The only lines you need to fill in manually are the when() stubs, since the tool reads your notifier but not your repository interface. Each stub is clearly marked:
// when(() => mockCartRepository.fetchCart())
// .thenAnswer((_) async => CartState(items: [], total: 0));
Uncomment and replace someMethod with the real method name from your repository. Everything else runs as-is.
Supported notifier types #
| Class | Detected |
|---|---|
AsyncNotifier<T> |
✅ |
AutoDisposeAsyncNotifier<T> |
✅ |
Notifier<T> |
✅ |
AutoDisposeNotifier<T> |
✅ |
Repository dependencies are detected via ref.read(someRepositoryProvider) and ref.watch(someRepositoryProvider) calls inside the notifier body.
Limitations #
- Only generates unit tests — no widget or integration tests
- Repository
when()stubs are scaffolded as comments; method names must be filled in manually - Requires notifiers to live in
presentation/notifiers/— other paths are ignored - Does not support
familynotifiers with arguments yet
Dependencies #
| Package | Used for |
|---|---|
analyzer |
Dart AST parsing (no build runner needed) |
dart_style |
Formatting generated test files |
args |
CLI flag parsing |
glob |
Recursive file discovery |
path |
Cross-platform path manipulation |
yaml |
Reading pubspec.yaml for package name |