Facade Testing
Magic ships in-memory fakes for every stateful facade so tests never touch real storage, network, or platform channels.
- Introduction
- Fake Catalog
- Auth.fake()
- Cache.fake()
- Vault.fake()
- Log.fake()
- Echo.fake()
- Full Example
- Unfaking
Introduction
Magic provides a first-class facade faking API for Auth, Cache, Vault, Log, Http, and Echo. 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, network traffic, or WebSocket connections are 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();
final echoFake = Echo.fake();
});
tearDown(() {
Auth.unfake();
Cache.unfake();
Vault.unfake();
Log.unfake();
Echo.unfake();
});
Each fake() call returns its fake instance so you can run assertions after the code under test executes.
Fake Catalog
| Facade | fake() return type |
Unfake | Docs |
|---|---|---|---|
Auth |
FakeAuthManager |
Auth.unfake() |
Auth.fake() |
Cache |
FakeCacheManager |
Cache.unfake() |
Cache.fake() |
Vault |
FakeVaultService |
Vault.unfake() |
Vault.fake() |
Log |
FakeLogManager |
Log.unfake() |
Log.fake() |
Http |
FakeNetworkDriver |
Http.unfake() |
HTTP Tests |
Echo |
FakeBroadcastManager |
Echo.unfake() |
Echo.fake() |
All fake types are exported from package:magic/testing.dart. Import them alongside package:magic/magic.dart in test files.
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();
Echo.fake()
Echo.fake() replaces the real BroadcastManager with a FakeBroadcastManager that routes all broadcasting operations through an in-memory FakeBroadcastDriver. No WebSocket connection is opened; all channel subscriptions and interceptor registrations are recorded in memory.
Signature:
static FakeBroadcastManager fake()
Basic Usage
test('controller subscribes to orders channel', () async {
final fake = Echo.fake();
Echo.channel('orders');
fake.assertSubscribed('orders'); // channel() records the subscription only
// The fake stays disconnected until connect() is awaited (the real Reverb
// driver likewise queues subscriptions until the socket is connected).
fake.assertDisconnected();
await Echo.connect();
fake.assertConnected();
});
Assertions
| Method | Description |
|---|---|
fake.assertConnected() |
Assert that the fake driver is currently connected. |
fake.assertDisconnected() |
Assert that the fake driver is currently disconnected. |
fake.assertSubscribed(String channel) |
Assert that channel is in the subscribed channels list. |
fake.assertNotSubscribed(String channel) |
Assert that channel is NOT in the subscribed channels list. |
fake.assertInterceptorAdded() |
Assert that at least one interceptor has been added to the driver. |
All assertion methods throw AssertionError with a descriptive message on failure.
test('broadcast interceptor is registered', () {
final fake = Echo.fake();
Echo.addInterceptor(MyLoggingInterceptor());
fake.assertInterceptorAdded();
});
test('controller leaves channel on dispose', () {
final fake = Echo.fake();
Echo.channel('orders');
fake.assertSubscribed('orders');
Echo.leave('orders');
fake.assertNotSubscribed('orders');
});
test('connect and disconnect lifecycle', () async {
final fake = Echo.fake();
fake.assertDisconnected();
await Echo.connect();
fake.assertConnected();
await Echo.disconnect();
fake.assertDisconnected();
});
Low-level Driver Inspection
fake.driver exposes the underlying FakeBroadcastDriver for direct inspection:
final fake = Echo.fake();
Echo.channel('orders');
Echo.private('user.1');
Echo.join('room.1');
expect(fake.driver.subscribedChannels, containsAll(['orders', 'private-user.1', 'presence-room.1']));
expect(fake.driver.isConnected, isFalse);
Resetting State
Call fake.reset() to clear all recorded state on the driver without restoring the real binding:
fake.reset(); // Clears connected state, subscribed channels, and interceptors
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();
Echo.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