Facade Testing
Introduction
Magic provides a first-class facade faking API for Auth, Cache, Vault, and Log. Each facade exposes a fake() static method that swaps the IoC-bound service with an in-memory implementation for the duration of a test. No real credentials, storage, or output is touched.
Call fake() in setUp and unfake() in tearDown:
setUp(() {
MagicApp.reset();
Magic.flush();
final authFake = Auth.fake();
final cacheFake = Cache.fake();
final vaultFake = Vault.fake();
final logFake = Log.fake();
});
tearDown(() {
Auth.unfake();
Cache.unfake();
Vault.unfake();
Log.unfake();
});
Each fake() call returns its fake instance so you can run assertions after the code under test executes.
Auth.fake()
Auth.fake() replaces the real AuthManager with a FakeAuthManager that routes all guard operations through an in-memory _FakeGuard. No platform channels, no secure storage, no token refresh calls.
Signature:
static FakeAuthManager fake({Authenticatable? user})
Pass user to pre-authenticate before the test body runs.
Basic Usage
test('dashboard redirects guests to login', () {
Auth.fake(); // No user — guest state
expect(Auth.check(), isFalse);
expect(Auth.guest, isTrue);
});
test('user is pre-authenticated', () {
final user = User()..fill({'id': 1, 'name': 'Alice'});
Auth.fake(user: user);
expect(Auth.check(), isTrue);
expect(Auth.user()?.name, 'Alice');
});
Assertions
| Method | Description |
|---|---|
fake.assertLoggedIn() |
Assert a user is currently authenticated. |
fake.assertLoggedOut() |
Assert no user is currently authenticated. |
fake.assertLoginAttempted() |
Assert at least one login call was made. |
fake.assertLoginCount(int expected) |
Assert an exact number of login calls. |
test('controller logs in user on success', () async {
final user = User()..fill({'id': 1, 'name': 'Alice'});
final fake = Auth.fake();
await Auth.login({'token': 'test-token'}, user);
fake.assertLoggedIn();
fake.assertLoginAttempted();
fake.assertLoginCount(1);
});
test('controller logs out user', () async {
final user = User()..fill({'id': 1, 'name': 'Alice'});
final fake = Auth.fake(user: user);
await Auth.logout();
fake.assertLoggedOut();
});
Resetting State
Call fake.reset() to clear the current user, token, and login attempt records without restoring the real driver:
fake.reset(); // Clears user, token, and login attempt history
Cache.fake()
Cache.fake() replaces the real CacheManager with a FakeCacheManager backed by a plain Map. All operations are synchronous and in-memory. Every put, get, and forget call is recorded in fake.recorded.
Signature:
static FakeCacheManager fake()
Basic Usage
test('controller caches user list', () async {
final fake = Cache.fake();
await Cache.put('users', ['Alice', 'Bob'], ttl: Duration(minutes: 5));
expect(Cache.has('users'), isTrue);
expect(Cache.get('users'), equals(['Alice', 'Bob']));
});
Assertions
| Method | Description |
|---|---|
fake.assertHas(String key) |
Assert that the key currently exists in the cache. |
fake.assertMissing(String key) |
Assert that the key does not exist in the cache. |
fake.assertPut(String key) |
Assert that the key was stored via put at least once. |
test('user list is cached after fetch', () async {
final fake = Cache.fake();
await Cache.put('users', ['Alice', 'Bob']);
fake.assertHas('users');
fake.assertPut('users');
});
test('cache is cleared after flush', () async {
final fake = Cache.fake();
await Cache.put('users', ['Alice']);
await Cache.flush();
fake.assertMissing('users');
});
Recorded Operations
Access fake.recorded for a full chronological list of cache operations:
final fake = Cache.fake();
await Cache.put('a', 1);
await Cache.get('a');
await Cache.forget('a');
expect(fake.recorded[0].operation, 'put');
expect(fake.recorded[1].operation, 'get');
expect(fake.recorded[2].operation, 'forget');
Each entry is a CacheRecord record type: ({String operation, String key, dynamic value}).
Resetting State
Call fake.reset() to clear both the in-memory store and the recorded operations list:
fake.reset();
Vault.fake()
Vault.fake() replaces the real MagicVaultService (backed by flutter_secure_storage) with a FakeVaultService that stores data in a plain Map. No platform channels are invoked.
Signature:
static FakeVaultService fake([Map initialValues = const {}])
Pass initialValues to pre-seed the vault before the test body runs.
Basic Usage
test('controller reads token from vault', () async {
Vault.fake({'auth_token': 'seed-token'});
final token = await Vault.get('auth_token');
expect(token, 'seed-token');
});
test('controller writes token to vault', () async {
final fake = Vault.fake();
await Vault.put('auth_token', 'abc123');
expect(await Vault.get('auth_token'), 'abc123');
});
Assertions
| Method | Description |
|---|---|
fake.assertWritten(String key) |
Assert that key was written via put at least once. |
fake.assertDeleted(String key) |
Assert that key was deleted via delete at least once. |
fake.assertContains(String key) |
Assert that key currently exists in the store. |
fake.assertMissing(String key) |
Assert that key does not currently exist in the store. |
test('logout clears auth token', () async {
final fake = Vault.fake({'auth_token': 'abc123'});
await Vault.delete('auth_token');
fake.assertDeleted('auth_token');
fake.assertMissing('auth_token');
});
test('login stores token in vault', () async {
final fake = Vault.fake();
await Vault.put('auth_token', 'new-token');
fake.assertWritten('auth_token');
fake.assertContains('auth_token');
});
Recorded Operations
Access fake.recorded for a full list of vault operations:
final fake = Vault.fake();
await Vault.put('token', 'abc');
await Vault.get('token');
await Vault.delete('token');
expect(fake.recorded[0].operation, 'put');
expect(fake.recorded[1].operation, 'get');
expect(fake.recorded[2].operation, 'remove');
Each entry is a VaultOperation record type: ({String operation, String key}).
Resetting State
Call fake.reset() to clear the in-memory store and recorded operations:
fake.reset();
Log.fake()
Log.fake() replaces the real LogManager with a FakeLogManager that captures all log entries in memory instead of writing to the console.
Signature:
static FakeLogManager fake()
Basic Usage
test('controller logs error on failure', () async {
final fake = Log.fake();
Log.error('Payment failed', {'order': 42});
fake.assertLoggedError('Payment failed');
});
Assertions
| Method | Description |
|---|---|
fake.assertLogged(String level, String message) |
Assert at least one entry matches both level and message. |
fake.assertLoggedError(String message) |
Shorthand for assertLogged('error', message). |
fake.assertNothingLogged([String? level]) |
Assert no entries recorded. If level is given, assert no entries at that level. |
fake.assertLoggedCount(int expected) |
Assert an exact total number of entries recorded. |
test('no logs emitted during normal operation', () {
final fake = Log.fake();
doNormalWork();
fake.assertNothingLogged();
});
test('exactly one error is logged', () async {
final fake = Log.fake();
Log.error('Something failed');
fake.assertLoggedCount(1);
fake.assertLoggedError('Something failed');
});
test('warning is logged at correct level', () {
final fake = Log.fake();
Log.warning('Rate limit approaching');
fake.assertLogged('warning', 'Rate limit approaching');
fake.assertNothingLogged('error'); // No errors logged
});
Inspecting Entries
Access fake.entries for the full list of captured log entries:
final fake = Log.fake();
Log.info('User logged in', {'id': 1});
Log.error('Token expired');
expect(fake.entries.length, 2);
expect(fake.entries[0].level, 'info');
expect(fake.entries[0].message, 'User logged in');
expect(fake.entries[1].level, 'error');
Each entry is a FakeLogEntry record type: ({String level, String message, dynamic context}).
Resetting State
Call fake.reset() to clear all captured entries without restoring the real driver:
fake.reset();
Full Example
A controller that integrates Auth, Cache, Vault, and Log, with tests covering all four fakes:
// lib/controllers/session_controller.dart
class SessionController extends MagicController {
Future login(String token, User user) async {
await Auth.login({'token': token}, user);
await Vault.put('auth_token', token);
await Cache.put('current_user', user.toMap());
Log.info('User logged in', {'id': user.id});
}
Future logout() async {
await Auth.logout();
await Vault.delete('auth_token');
await Cache.forget('current_user');
Log.info('User logged out');
}
}
// test/http/session_controller_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:magic/magic.dart';
void main() {
group('SessionController', () {
late SessionController controller;
late FakeAuthManager authFake;
late FakeCacheManager cacheFake;
late FakeVaultService vaultFake;
late FakeLogManager logFake;
setUp(() {
MagicApp.reset();
Magic.flush();
controller = SessionController();
authFake = Auth.fake();
cacheFake = Cache.fake();
vaultFake = Vault.fake();
logFake = Log.fake();
});
tearDown(() {
Auth.unfake();
Cache.unfake();
Vault.unfake();
Log.unfake();
});
test('login authenticates user and stores credentials', () async {
final user = User()..fill({'id': 1, 'name': 'Alice'});
await controller.login('token-abc', user);
authFake.assertLoggedIn();
authFake.assertLoginCount(1);
vaultFake.assertWritten('auth_token');
vaultFake.assertContains('auth_token');
cacheFake.assertHas('current_user');
cacheFake.assertPut('current_user');
logFake.assertLogged('info', 'User logged in');
});
test('logout clears all session state', () async {
final user = User()..fill({'id': 1, 'name': 'Alice'});
authFake = Auth.fake(user: user);
vaultFake = Vault.fake({'auth_token': 'token-abc'});
await controller.logout();
authFake.assertLoggedOut();
vaultFake.assertDeleted('auth_token');
vaultFake.assertMissing('auth_token');
cacheFake.assertMissing('current_user');
logFake.assertLogged('info', 'User logged out');
});
});
}
Unfaking
Call unfake() in tearDown to remove the fake from the IoC container and restore the real service binding for subsequent tests:
tearDown(() {
Auth.unfake();
Cache.unfake();
Vault.unfake();
Log.unfake();
});
After unfake(), the next facade call resolves the original singleton binding as if fake() was never called. This is the recommended pattern when each test should start from a clean real-service state.
If you want to reuse the same fake across multiple tests in a group without restoring the real driver, call fake.reset() instead:
final logFake = Log.fake(); // Install once for the group
test('first', () {
Log.error('a');
logFake.assertLoggedCount(1);
logFake.reset(); // Clear for next test — fake remains installed
});
test('second', () {
Log.error('b');
logFake.assertLoggedCount(1);
});
See Also
- HTTP Tests —
Http.fake(), URL pattern stubs, request assertions