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.

Copyright © 2026