Testing: Getting Started
- Introduction
- Setting Up Tests
- Writing Tests
- Controller Testing
- Model Testing
- Mock HTTP Responses
- Running Tests
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
Create a test/test_helper.dart for common setup:
import 'package:flutter_test/flutter_test.dart';
import 'package:fluttersdk_magic/fluttersdk_magic.dart';
Future setupTestEnvironment() async {
TestWidgetsFlutterBinding.ensureInitialized();
await Magic.init(
envFileName: '.env.testing',
configFactories: [
() => appConfig,
() => testDatabaseConfig,
],
);
}
Map get testDatabaseConfig => {
'database': {
'default': 'sqlite',
'connections': {
'sqlite': {
'driver': 'sqlite',
'database': ':memory:', // In-memory for tests
},
},
},
};
Writing Tests
Basic Test Structure
import 'package:flutter_test/flutter_test.dart';
import '../test_helper.dart';
void main() {
setUpAll(() async {
await setupTestEnvironment();
});
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
[!TIP] Use the
:memory:database driver for faster tests that don't persist to disk.