Guides
Testing
Test code that uses Authyra with AuthyraClient and InMemoryStorage, no singleton, no mocking framework.
AuthyraClient is dependency-injected and holds no global state, so tests don't need a singleton, Authyra.initialize(), or a mocking framework. A fake provider and InMemoryStorage are usually enough.
Basic setup
import 'package:test/test.dart';
import 'package:authyra/authyra.dart';
void main() {
late AuthyraClient client;
setUp(() async {
client = AuthyraClient(
providers: [
CredentialsProvider(
id: 'email',
authorize: (creds) async {
if (creds?.password == 'correct') {
return AuthUser(id: '1', email: creds!.email);
}
return null;
},
),
],
storage: InMemoryStorage(),
);
await client.initialize();
});
test('returns user on valid credentials', () async {
final user = await client.signIn('email', params: CredentialsSignInParams(
email: 'alice@example.com',
password: 'correct',
));
expect(user.email, 'alice@example.com');
});
test('throws on invalid credentials', () async {
expect(
() => client.signIn('email', params: CredentialsSignInParams(
email: 'alice@example.com',
password: 'wrong',
)),
throwsA(isA<AuthenticationFailedException>()),
);
});
}
Use a new InMemoryStorage instance per test to guarantee isolation; it holds state in a plain map with no cross-test cleanup otherwise.
Testing token refresh
Give the fake provider supportsRefresh: true and a refreshToken implementation:
class _FakeRefreshableProvider implements AuthProvider {
int refreshCallCount = 0;
@override String get id => 'fake';
@override bool get supportsRefresh => true;
@override bool get supportsSignOut => false;
@override
Future<AuthSignInResult?> signIn({AuthSignInParams? params}) async {
return AuthSignInResult(
user: AuthUser(id: '1', email: 'alice@example.com'),
accessToken: 'at_1',
refreshToken: 'rt_1',
expiresAt: DateTime.now().add(const Duration(minutes: 1)),
);
}
@override
Future<AuthTokenResult?> refreshToken(String refreshToken) async {
refreshCallCount++;
return AuthTokenResult(
accessToken: 'at_2',
expiresAt: DateTime.now().add(const Duration(hours: 1)),
);
}
@override
Future<void> signOut({String? userId}) async {}
}
test('refreshes the token on demand', () async {
final provider = _FakeRefreshableProvider();
final client = AuthyraClient(providers: [provider], storage: InMemoryStorage());
await client.initialize();
await client.signIn('fake');
final success = await client.refreshSession();
expect(success, isTrue);
expect(provider.refreshCallCount, 1);
});
Testing multi-account flows
test('switches between two signed-in accounts', () async {
final client = AuthyraClient(providers: [provider], storage: InMemoryStorage());
await client.initialize();
await client.signIn('fake'); // registers user A, now active
// ... sign in user B via another provider or a second call ...
await client.accounts.switchTo(userAId);
expect(client.accounts.active?.id, userAId);
});
Testing events
test('emits SignInEvent on success', () async {
SignInEvent? captured;
client.events.on<SignInEvent>((e) => captured = e);
await client.signIn('email', params: CredentialsSignInParams(
email: 'alice@example.com', password: 'correct',
));
expect(captured, isNotNull);
expect(captured!.user.email, 'alice@example.com');
});
Testing callback denials
class _DenyAllCallbacks extends AuthCallbacks {
@override
Future<CallbackResult> onBeforeSignIn(String providerName, AuthSignInParams? params) async {
return const CallbackResult.deny('blocked for test');
}
}
test('a denied sign-in throws and persists nothing', () async {
final client = AuthyraClient(
providers: [provider],
storage: InMemoryStorage(),
callbacks: _DenyAllCallbacks(),
);
await client.initialize();
expect(() => client.signIn('fake'), throwsA(isA<AuthenticationFailedException>()));
expect(await client.isAuthenticated(), isFalse);
});
If your code uses Authyra.instance
The singleton needs explicit teardown between tests:
tearDown(() async {
if (Authyra.isInitialized) {
await Authyra.instance.dispose();
}
});
Prefer injecting AuthyraClient directly wherever you can; it sidesteps this entirely.