Skip to content

Facade Testing

View raw .md

Magic ships in-memory fakes for every stateful facade so tests never touch real storage, network, or platform channels.

On this page

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:

dart
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. login() and logout() still dispatch AuthLogin/AuthLogout through the real Event facade, so a listener registered with Event.listen observes a faked session the same way it would a real one.

Signature:

dart
static FakeAuthManager fake({Authenticatable? user})

Pass user to pre-authenticate before the test body runs.

Basic Usage#

dart
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<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.
dart
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:

dart
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:

dart
static FakeCacheManager fake()

Basic Usage#

dart
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.
dart
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:

dart
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:

dart
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:

dart
static FakeVaultService fake([Map<String, String> initialValues = const {}])

Pass initialValues to pre-seed the vault before the test body runs.

Basic Usage#

dart
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.
dart
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:

dart
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:

dart
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:

dart
static FakeLogManager fake()

Basic Usage#

dart
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.
dart
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:

dart
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:

dart
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:

dart
static FakeBroadcastManager fake()

Basic Usage#

dart
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.

dart
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:

dart
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:

dart
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:

dart
// lib/controllers/session_controller.dart
class SessionController extends MagicController {
  Future<void> 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<void> 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:

dart
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:

dart
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
esc

Search index did not load.

No match for "".

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