Skip to main content

mogen_unit_tests 1.1.4 copy "mogen_unit_tests: ^1.1.4" to clipboard
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 via ref.read(...)/ref.watch(...) — repositories, services, clients, and other notifiers alike
  • a ProviderContainer with dependency overrides in setUp
  • any dependency calls made from build() stubbed once at the bottom of setUp (after the overrides), so initializing the notifier never throws MissingStubError — and so those stubs aren't repeated identically inside every method's test
  • container.dispose() in tearDown
  • 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 (no Fake classes, no registerFallbackValue)
  • optional state field assertions when stateInfo is 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.dart defines AppException with a factory AppException.test() constructor — this is what error-path stubs throw.
  • Repositories: interfaces live at lib/features/<feature>/domain/repositories/<name>.dart, with an implementation at lib/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 routerProvider dependency is assumed to expose go_router's GoRouter, not Flutter's own Router class.

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
  • family notifiers 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_tests from this repository itself will not produce output unless you point it at a consuming Flutter app with lib/features/....
  • The current output destination is test/unit/features/..., which is now reflected in the examples above.
1
likes
0
points
133
downloads

Publisher

unverified uploader

Weekly Downloads

Scans Riverpod AsyncNotifier classes and auto-generates Mocktail unit tests.

Homepage
Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

analyzer, args, dart_style, glob, path, yaml

More

Packages that depend on mogen_unit_tests