# Facade Testing
- [Introduction](#introduction)
- [Auth.fake()](#auth-fake)
- [Cache.fake()](#cache-fake)
- [Vault.fake()](#vault-fake)
- [Log.fake()](#log-fake)
- [Full Example](#full-example)
- [Unfaking](#unfaking)
## 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`:
```dart
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:**
```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()?.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 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();
```
## 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 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:
```dart
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:
```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-tests.md) — `Http.fake()`, URL pattern stubs, request assertions