Testing: Getting Started
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#
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:
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:
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:
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:
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#
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:
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#
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:
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:
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#
flutter test
Run Specific Test File#
flutter test test/unit/models/user_test.dart
Run With Coverage#
flutter test --coverage
Test Output#
flutter test --reporter=expanded