Skip to content

Testing

View raw .md

Test code that persists to SQLite against an in-memory database per test, run migrations and factories in it, assert with DB, and fake the store when a test needs no database at all.

On this page

In-Memory Database#

A useLocal model resolves a store before every local leg, so each test binds one: the plugin's QueryModelStore over an in-memory SQLite connection. Add the sqlite3 package as a dev dependency to open the connection:

bash
flutter pub add --dev sqlite3
dart
import 'package:flutter_test/flutter_test.dart';
import 'package:magic/magic.dart';
import 'package:magic_local_storage/magic_local_storage.dart';
import 'package:sqlite3/sqlite3.dart';

void main() {
  setUp(() {
    MagicApp.reset();
    Magic.flush();

    DatabaseManager().setConnection(sqlite3.openInMemory());
    MagicApp.instance.singleton('local_model_store', () => QueryModelStore());
  });

  tearDown(() {
    DatabaseManager().dispose();
  });

  test('the connection is open', () {
    expect(DatabaseManager().isInitialized, isTrue);
    expect(DB.select('SELECT 1 AS one').single['one'], 1);
  });
}

DatabaseServiceProvider(MagicApp.instance).register() binds db and local_model_store together if you prefer it to the manual line; do not call its boot(), which opens the file database. The name :memory: does not work as the database config value on native platforms, because the factory joins it onto the documents directory.

DatabaseManager is a singleton that Magic.flush() does not touch: set a connection in setUp and call dispose() in tearDown, so no test sees another test's tables.

Running Migrations#

A fresh connection has no tables, so run the migrations the test needs. The ledger lives in the same connection, which makes every test start from a clean slate.

dart
import 'package:flutter_test/flutter_test.dart';
import 'package:magic/magic.dart';
import 'package:magic_local_storage/magic_local_storage.dart';
import 'package:sqlite3/sqlite3.dart';

class CreateItemsTable extends Migration {
  @override
  String get name => '20261114120000_create_items_table';

  @override
  void up() {
    Schema.create('items', (Blueprint table) {
      table.id();
      table.string('name');
    });
  }

  @override
  void down() {
    Schema.dropIfExists('items');
  }
}

void main() {
  setUp(() async {
    MagicApp.reset();
    Magic.flush();

    DatabaseManager().setConnection(sqlite3.openInMemory());
    await Migrator().run([CreateItemsTable()]);
  });

  tearDown(() {
    DatabaseManager().dispose();
  });

  test('the table exists', () async {
    expect(await Schema.hasTable('items'), isTrue);
    expect(await Migrator().getPending([CreateItemsTable()]), isEmpty);
  });
}

Models and Factories#

With the store bound and the tables created, a model and a factory behave as in the app:

dart
import 'package:flutter_test/flutter_test.dart';
import 'package:magic/magic.dart';
import 'package:magic_local_storage/magic_local_storage.dart';
import 'package:sqlite3/sqlite3.dart';

class Item extends Model with InteractsWithPersistence {
  @override
  String get table => 'items';

  @override
  String get resource => 'items';

  @override
  List<String> get fillable => ['name'];

  @override
  bool get useLocal => true;

  @override
  bool get useRemote => false;

  String? get name => getAttribute('name') as String?;

  static Future<Item?> find(dynamic id) => InteractsWithPersistence.findById<Item>(id, Item.new);
}

class ItemFactory extends Factory<Item> {
  @override
  Factory<Item> newFactory() => ItemFactory();

  @override
  Item newInstance() => Item();

  @override
  Map<String, dynamic> definition() => {'name': faker.lorem.word()};
}

void main() {
  setUp(() {
    MagicApp.reset();
    Magic.flush();

    final db = sqlite3.openInMemory()..execute('CREATE TABLE items (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)');
    DatabaseManager().setConnection(db);
    MagicApp.instance.singleton('local_model_store', () => QueryModelStore());
  });

  tearDown(() {
    DatabaseManager().dispose();
  });

  test('a factory creates and a model finds', () async {
    final Item created = (await ItemFactory().state({'name': 'Anvil'}).create()).first;

    final Item? found = await Item.find(created.id);

    expect(found?.name, 'Anvil');
  });

  test('a model without a bound store fails loudly', () async {
    MagicApp.reset();
    Magic.flush();

    await expectLater(Item.find(1), throwsStateError);
  });
}

A seeder run goes through Log, so call Log.fake() in that test's setUp.

Database Assertions#

Assert against the table with DB.table:

dart
import 'package:flutter_test/flutter_test.dart';
import 'package:magic_local_storage/magic_local_storage.dart';

import 'package:sqlite3/sqlite3.dart';

void main() {
  setUp(() {
    DatabaseManager().setConnection(sqlite3.openInMemory());
    DB.statement('CREATE TABLE items (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)');
  });

  tearDown(() {
    DatabaseManager().dispose();
  });

  test('rows reach the table', () async {
    await DB.table('items').insertAll([
      {'name': 'Anvil'},
      {'name': 'Bolt'},
    ]);

    expect(await DB.table('items').count(), 2);
    expect(await DB.table('items').where('name', 'Anvil').exists(), isTrue);

    final Map<String, dynamic>? row = await DB.table('items').where('id', 1).first();
    expect(row?['name'], 'Anvil');
  });
}

Faking the Store#

A test for code that only needs a useLocal model to save, and not a SQL engine, binds a fake LocalModelStore. Implement the contract, see the sample under Custom Stores, and bind it:

dart
import 'package:magic/magic.dart';

void bindFake(LocalModelStore store) {
  MagicApp.instance.singleton('local_model_store', () => store);
}

There is no mocking library: implement the abstract class, as with every Magic contract.

Testing a Feed#

A SyncFeed talks to Http and to the ledger. Fake the network with Http.fake, create the sync_cursors table with CreateSyncCursorsTable, run the feed and read the bookmarks back:

dart
import 'package:magic/magic.dart';
import 'package:magic_local_storage/magic_local_storage.dart';

Future<void> checkSync(SyncFeed feed) async {
  Http.fake((MagicRequest request) {
    return Http.response({'data': <Object?>[], 'has_more': false}, 200);
  });

  await Migrator().run([CreateSyncCursorsTable()]);

  final SyncReport report = await feed.run(scope: 'team-1', account: 'user-1');
  final SyncBookmarks bookmarks = const SyncLedger().read(scope: 'team-1', feed: 'items');

  print('${report.complete} ${bookmarks.pushMark}');

  Http.unfake();
}

Resetting Static State#

Magic.flush() and MagicApp.reset() clear the container, not the statics this package keeps. Reset them yourself.

State Reset
The DatabaseManager connection and schema cache DatabaseManager().dispose() in tearDown; set a new connection in setUp.
A faked network Http.unfake() after Http.fake.
The faked log Log.fake() again in setUp; the container reset drops it.

Call MagicApp.reset() and Magic.flush() in every setUp, before you bind a store: the reset also clears the binding.

  • Models: the store contract and the StateError.
  • Migrations: the migrations a test runs.
  • Seeding: factories and seeders.
  • Sync: the feed under test.
esc

Search index did not load.

No match for "".

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