Skip to content

Testing: Getting Started

View raw .md

Magic is built with testing in mind: it integrates with Dart's test package and provides helpers for resetting the IoC container, faking facades, and asserting controller and model behavior.

On this page

Introduction#

Magic is built with testing in mind from the ground up. The framework provides a testing foundation that works seamlessly with Dart's built-in test package, along with helpers for testing controllers, models, and HTTP interactions.

Setting Up Tests#

Directory Structure#

text
test/
├── unit/
│   ├── models/
│   │   └── user_test.dart
│   └── controllers/
│       └── auth_controller_test.dart
├── feature/
│   └── authentication_test.dart
└── test_helper.dart

Test Helper#

Magic provides a MagicTest bootstrap helper via package:magic/testing.dart. Call MagicTest.init() once at the top of main() — it registers setUpAll, setUp, and tearDown automatically:

dart
import 'package:flutter_test/flutter_test.dart';
import 'package:magic/testing.dart';

void main() {
  MagicTest.init(); // Registers setUpAll + setUp + tearDown automatically

  test('my test', () {
    // Magic container is clean, facades are reset
  });
}

MagicTest.init() handles:

  • setUpAll: TestWidgetsFlutterBinding.ensureInitialized()
  • setUp: MagicApp.reset() + Magic.flush() + Gate.flush()
  • tearDown: Magic.flush() + Gate.flush() + DateManager.reset() + Translator.reset(), so a loaded catalogue or locale does not carry into the next test

Gate is a process static that Magic.flush() does not clear on its own, so without the extra call an ability defined in one test leaks into every later test in the same file.

For integration tests that need a full Magic.init() lifecycle, use MagicTest.boot() instead:

dart
import 'package:magic/testing.dart';

void main() {
  setUpAll(() async {
    await MagicTest.boot(
      configs: [testDatabaseConfig],
      envFileName: '.env.testing',
    );
  });

  test('full lifecycle test', () async {
    // Magic is fully initialised with providers booted
  });
}

Map<String, dynamic> get testDatabaseConfig => {
  'database': {
    'default': 'sqlite',
    'connections': {
      'sqlite': {
        'driver': 'sqlite',
        'database': ':memory:', // In-memory for tests
      },
    },
  },
};

Booting a Handful of Providers#

A widget test that needs real providers but no env file or config boots them with MagicTest.bootProviders(providers). It reproduces Magic.init's order: every register() first, then every boot(), both in list order. Pair it with MagicTest.disposeBooted() in tearDown:

dart
setUp(() async {
  await MagicTest.bootProviders([
    AppServiceProvider(Magic.app),
    AuthServiceProvider(Magic.app),
  ]);
});

tearDown(MagicTest.disposeBooted);

disposeBooted() undoes what Magic.flush() does not reach: it disposes the registered controllers (newest first), detaches SessionScope from auth state, resets MagicRouter, then flushes the container. Anything an app keeps in its own statics, such as a timer a provider arms in boot(), stays the app's to reset.

Loading a Translation Catalogue#

A widget test that renders trans() output needs a real catalogue loaded first; without one, trans() renders the raw dotted key. MagicTest.loadTranslations(locale, {directory}) reads <directory>/<locale>.json off disk (default assets/lang, the app's own catalogue location), flattens it the same way the app's own loader does, and installs it before awaiting the translator's load:

dart
await MagicTest.loadTranslations('tr');
expect(trans('auth.login'), 'Giriş yap');

Point directory at a temp directory with a small fixture json to test one key in isolation without touching the app's real catalogue. Loading never requires a fallback-locale file on disk: the translator only warns and falls back to an empty map when the fallback catalogue fails to load.

Writing Tests#

Basic Test Structure#

dart
import 'package:flutter_test/flutter_test.dart';
import 'package:magic/testing.dart';

void main() {
  MagicTest.init();

  group('User', () {
    test('can create a user', () async {
      final user = User()
        ..fill({
          'name': 'John Doe',
          'email': '[email protected]',
        });
      
      await user.save();
      
      expect(user.id, isNotNull);
      expect(user.name, equals('John Doe'));
    });

    test('validates email format', () {
      final validator = EmailValidator();
      
      expect(validator.passes('[email protected]'), isTrue);
      expect(validator.passes('invalid-email'), isFalse);
    });
  });
}

Controller Testing#

Test controllers by creating instances and calling methods:

dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';

void main() {
  late AuthController controller;
  
  setUp(() {
    controller = AuthController();
  });

  tearDown(() {
    controller.dispose();
  });

  group('AuthController', () {
    test('login sets loading state', () async {
      expect(controller.isLoading, isFalse);
      
      // Start login (will fail due to no mock, but tests state)
      controller.login({'email': '[email protected]', 'password': 'password'});
      
      expect(controller.isLoading, isTrue);
    });

    test('clearErrors removes all validation errors', () {
      controller.setError('email', 'Invalid email');
      expect(controller.hasError('email'), isTrue);
      
      controller.clearErrors();
      expect(controller.hasError('email'), isFalse);
    });
  });
}

Testing State Changes#

dart
test('setSuccess updates state correctly', () {
  final controller = TaskController();
  
  expect(controller.isLoading, isFalse);
  expect(controller.isSuccess, isFalse);
  
  controller.setLoading();
  expect(controller.isLoading, isTrue);
  
  controller.setSuccess([Task(name: 'Test')]);
  expect(controller.isSuccess, isTrue);
  expect(controller.data, isNotEmpty);
});

Model Testing#

Test models using factories for consistent data:

dart
import 'package:flutter_test/flutter_test.dart';

void main() {
  setUpAll(() async {
    await setupTestEnvironment();
    await Migrator().run([CreateUsersTable()]);
  });

  group('User Model', () {
    test('creates user with factory', () async {
      final users = await UserFactory().count(5).create();
      
      expect(users.length, equals(5));
      expect(users.first.id, isNotNull);
    });

    test('finds user by id', () async {
      final created = (await UserFactory().create()).first;
      final found = await User.find(created.id);
      
      expect(found, isNotNull);
      expect(found!.email, equals(created.email));
    });

    test('updates user attributes', () async {
      final user = (await UserFactory().create()).first;
      
      user.name = 'Updated Name';
      await user.save();
      
      final fresh = await User.find(user.id);
      expect(fresh!.name, equals('Updated Name'));
    });

    test('deletes user', () async {
      final user = (await UserFactory().create()).first;
      final id = user.id;
      
      await user.delete();
      
      final found = await User.find(id);
      expect(found, isNull);
    });
  });
}

Mock HTTP Responses#

Use mock HTTP responses for testing API interactions:

dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class MockHttpClient extends Mock implements HttpClient {}

void main() {
  late MockHttpClient mockHttp;
  late AuthController controller;

  setUp(() {
    mockHttp = MockHttpClient();
    // Inject mock into Http facade
    Http.setTestClient(mockHttp);
    controller = AuthController();
  });

  test('login success redirects to dashboard', () async {
    // Arrange
    when(() => mockHttp.post('/login', data: any(named: 'data')))
        .thenAnswer((_) async => MagicResponse(
          statusCode: 200,
          body: {
            'token': 'test-token',
            'user': {'id': 1, 'name': 'John'},
          },
        ));

    // Act
    await controller.login({
      'email': '[email protected]',
      'password': 'password',
    });

    // Assert
    expect(Auth.check(), isTrue);
  });

  test('login failure shows error', () async {
    when(() => mockHttp.post('/login', data: any(named: 'data')))
        .thenAnswer((_) async => MagicResponse(
          statusCode: 401,
          body: {'message': 'Invalid credentials'},
        ));

    await controller.login({
      'email': '[email protected]',
      'password': 'wrong',
    });

    expect(controller.isError, isTrue);
  });
}

Running Tests#

Run All Tests#

bash
flutter test

Run Specific Test File#

bash
flutter test test/unit/models/user_test.dart

Run With Coverage#

bash
flutter test --coverage

Test Output#

bash
flutter test --reporter=expanded
esc

Search index did not load.

No match for "".

Try a class like rounded-lg or a widget like WButton.