Agent Skillstekartik/sqflite › sqflite-common-ffi-testing

sqflite-common-ffi-testing

GitHub

指导在 Dart VM 或 Flutter 测试环境中使用 sqflite_common_ffi 进行单元测试和 Widget 测试,通过 FFI 实现真实 SQLite 运行,替代模拟器,确保数据库代码在 CI 中可测。

sqflite_common_ffi/skills/sqflite-common-ffi-testing/SKILL.md tekartik/sqflite

Trigger Scenarios

编写或修复 sqflite 相关的单元测试 配置 Flutter 或 Dart VM 的 CI 数据库测试环境 解决 sqflite 在无设备环境下的测试问题

Install

npx skills add tekartik/sqflite --skill sqflite-common-ffi-testing -g -y
More Options

Non-standard path

npx skills add https://github.com/tekartik/sqflite/tree/master/sqflite_common_ffi/skills/sqflite-common-ffi-testing -g -y

Use without installing

npx skills use tekartik/sqflite@sqflite-common-ffi-testing

指定 Agent (Claude Code)

npx skills add tekartik/sqflite --skill sqflite-common-ffi-testing -a claude-code -g -y

安装 repo 全部 skill

npx skills add tekartik/sqflite --all -g -y

预览 repo 内 skill

npx skills add tekartik/sqflite --list

SKILL.md

Frontmatter
{
    "name": "sqflite-common-ffi-testing",
    "description": "Use when writing or fixing unit tests, widget tests or CI runs for code that uses sqflite (Dart VM or Flutter, no device or emulator) with package:sqflite_common_ffi: sqfliteFfiInit in main or setUpAll, opening inMemoryDatabasePath with databaseFactoryFfi, replacing the global databaseFactory so existing openDatabase code runs in flutter test, databaseFactoryFfiNoIsolate for testWidgets, @TestOn('vm'), temporary database files and deleteDatabase, asserting on DatabaseException."
}

Testing sqflite code with sqflite_common_ffi

The native sqflite plugin cannot run in dart test or flutter test. package:sqflite_common_ffi provides databaseFactoryFfi, a real SQLite implementation (via package:sqlite3) that runs on the developer machine and on Linux/macOS/Windows CI runners, so database code is tested against SQLite itself rather than mocks.

import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:test/test.dart';

void main() {
  sqfliteFfiInit();
  test('insert and query', () async {
    var db = await databaseFactoryFfi.openDatabase(inMemoryDatabasePath);
    await db.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)');
    await db.insert('Test', {'value': 'a'});
    expect(await db.query('Test'), [
      {'id': 1, 'value': 'a'},
    ]);
    await db.close();
  });
}

Guidelines

Setup

  • Add sqflite_common_ffi to dev_dependencies (to dependencies if the app itself uses it on desktop). sqlite3 >= 3 bundles SQLite through build hooks: run tests with dart test / flutter test, nothing to install.
  • Import package:sqflite_common_ffi/sqflite_ffi.dart in tests. It exports the whole sqflite_common API (Database, OpenDatabaseOptions, inMemoryDatabasePath, DatabaseException, global databaseFactory, openDatabase...). In Flutter tests it replaces package:sqflite/sqflite.dart (importing both triggers the unnecessary_import lint); both expose the same global databaseFactory.
  • Call sqfliteFfiInit() once, at the top of main() or in setUpAll. Required on Windows, harmless elsewhere.
  • Mark Dart VM test files with @TestOn('vm') (before library;) when the package also runs tests on the web: databaseFactoryFfi throws UnsupportedError in a browser.

Choosing the factory

  • Code that receives a DatabaseFactory (recommended design): pass databaseFactoryFfi and open inMemoryDatabasePath.
  • Code that calls the global openDatabase() / deleteDatabase() (typical Flutter code): set databaseFactory = databaseFactoryFfi; in setUpAll (or at the top of main). Do it once per test process; setting it again only prints a warning.
  • testWidgets (Flutter widget tests): use databaseFactoryFfiNoIsolate. The isolate-based factory can hang under the fake async zone of WidgetTester; the no-isolate factory runs SQLite synchronously in the test isolate.
  • Plain test() inside a Flutter package works with either factory; databaseFactoryFfi is closest to production.

Isolation between tests

  • Prefer inMemoryDatabasePath: each openDatabase after a close() gives an empty database and nothing is left on disk. Always close() in the test (or in tearDown) so the next test starts fresh.
  • For file-based tests (migrations, onUpgrade, reopen scenarios) use a unique relative name per test: it resolves under .dart_tool/sqflite_common_ffi/databases, is created on open and is ignored by git in most setups. Call factory.deleteDatabase(path) in setUp so a previous run cannot leak state.
  • Do not share one Database across tests through a global; open it in setUp and close it in tearDown.

Assertions

  • Errors are DatabaseException: assert with isNoSuchTableError('Test'), isUniqueConstraintError('Test.value'), isSyntaxError(), isDatabaseClosedError() or getResultCode(), never on message text.
  • expect(await db.query(...), [...]) works: rows are List<Map<String, Object?>> and deep-equal to literal maps.
  • await db.getVersion() returns the user_version and is a quick check that onCreate / onUpgrade ran.

CI

  • Linux, macOS and Windows runners all work with sqlite3 >= 3 without extra packages. Only the legacy sqlite3 v2 setup needed sudo apt-get install libsqlite3-dev on Ubuntu.
  • Keep test paths relative or under a temp directory; avoid absolute developer-machine paths.

Examples

Testing a class that takes a DatabaseFactory

import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:test/test.dart';

class NoteStore {
  NoteStore(this.factory);
  final DatabaseFactory factory;
  late Database db;

  Future<void> open(String path) async {
    db = await factory.openDatabase(
      path,
      options: OpenDatabaseOptions(
        version: 1,
        onCreate: (db, _) => db.execute(
          'CREATE TABLE Note (id INTEGER PRIMARY KEY, text TEXT NOT NULL)',
        ),
      ),
    );
  }

  Future<int> add(String text) => db.insert('Note', {'text': text});
  Future<List<String>> texts() async =>
      (await db.query('Note', orderBy: 'id')).map((r) => r['text'] as String).toList();
}

void main() {
  sqfliteFfiInit();
  late NoteStore store;

  setUp(() async {
    store = NoteStore(databaseFactoryFfi);
    await store.open(inMemoryDatabasePath);
  });
  tearDown(() => store.db.close());

  test('add', () async {
    await store.add('hello');
    expect(await store.texts(), ['hello']);
  });

  test('not null constraint', () async {
    try {
      await store.db.insert('Note', {'text': null});
      fail('should throw');
    } on DatabaseException catch (e) {
      expect(e.isNotNullConstraintError('Note.text'), isTrue);
    }
  });
}

Flutter test for code using the global openDatabase

import 'package:flutter_test/flutter_test.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';

void main() {
  setUpAll(() {
    sqfliteFfiInit();
    databaseFactory = databaseFactoryFfi;
  });

  test('openDatabase works in flutter test', () async {
    var db = await openDatabase(
      inMemoryDatabasePath,
      version: 1,
      onCreate: (db, version) async {
        await db.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)');
      },
    );
    await db.insert('Test', {'value': 'my_value'});
    expect(await db.query('Test'), [
      {'id': 1, 'value': 'my_value'},
    ]);
    await db.close();
  });
}

Widget test: no isolate

import 'package:flutter_test/flutter_test.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';

void main() {
  sqfliteFfiInit();
  databaseFactory = databaseFactoryFfiNoIsolate;

  testWidgets('database in a widget test', (tester) async {
    var db = await openDatabase(inMemoryDatabasePath);
    await db.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY)');
    expect(await db.query('Test'), isEmpty);
    await db.close();
  });
}

Migration test with a file that is reopened

@TestOn('vm')
library;

import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:test/test.dart';

void main() {
  sqfliteFfiInit();
  final factory = databaseFactoryFfi;
  const path = 'migration_test.db'; // under .dart_tool/sqflite_common_ffi/databases

  setUp(() => factory.deleteDatabase(path));

  test('upgrade from 1 to 2 adds a column', () async {
    var db = await factory.openDatabase(
      path,
      options: OpenDatabaseOptions(
        version: 1,
        onCreate: (db, _) => db.execute('CREATE TABLE Test (id INTEGER PRIMARY KEY)'),
      ),
    );
    await db.close();

    db = await factory.openDatabase(
      path,
      options: OpenDatabaseOptions(
        version: 2,
        onUpgrade: (db, oldVersion, newVersion) async {
          if (oldVersion < 2) {
            await db.execute('ALTER TABLE Test ADD COLUMN name TEXT');
          }
        },
      ),
    );
    expect(await db.getVersion(), 2);
    await db.insert('Test', {'name': 'ok'});
    await db.close();
  });
}

Expecting a missing table error

import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:test/test.dart';

void main() {
  sqfliteFfiInit();
  test('no such table', () async {
    var db = await databaseFactoryFfi.openDatabase(inMemoryDatabasePath);
    try {
      await db.query('Missing');
      fail('should throw');
    } on DatabaseException catch (e) {
      expect(e.isNoSuchTableError('Missing'), isTrue);
    } finally {
      await db.close();
    }
  });
}

Common mistakes

  • Forgetting sqfliteFfiInit(): tests pass on macOS/Linux and fail on Windows.
  • Using databaseFactoryFfi inside testWidgets: use databaseFactoryFfiNoIsolate.
  • Calling the global openDatabase() without setting databaseFactory first: StateError: databaseFactory not initialized.
  • Not closing databases between tests, so singleInstance returns the previous test's database (same path) with stale tables.
  • Reusing a file path across tests without deleteDatabase in setUp.
  • Running the test file in a browser configuration: add @TestOn('vm').
  • Matching on exception messages instead of DatabaseException helpers.

Version History

  • aaabf90 Current 2026-09-22 03:38

Same Skill Collection

packages/sqflite_common_ffi_async_test/skills/sqflite-common-ffi-async-test-suite/SKILL.md
packages/sqflite_common_ffi_async/skills/sqflite-common-ffi-async-factory/SKILL.md
sqflite_android/skills/sqflite-android-setup/SKILL.md
packages_flutter/sqflite_ffi/skills/sqflite-ffi-flutter/SKILL.md
packages_web/sqflite_common_ffi_web_test/skills/sqflite-common-ffi-web-test-suite/SKILL.md
packages_web/sqflite_common_ffi_web/skills/sqflite-common-ffi-web-options/SKILL.md
packages_web/sqflite_common_ffi_web/skills/sqflite-common-ffi-web-setup/SKILL.md
sqflite_common_ffi/skills/sqflite-common-ffi-desktop/SKILL.md
sqflite_common_test/skills/sqflite-common-test-suite/SKILL.md
sqflite_common/skills/sqflite-common-api/SKILL.md
sqflite_darwin/skills/sqflite-darwin-setup/SKILL.md
sqflite_platform_interface/skills/sqflite-platform-interface-implementers/SKILL.md
sqflite/skills/sqflite-crud-and-transactions/SKILL.md
sqflite/skills/sqflite-open-database/SKILL.md
sqflite/skills/sqflite-testing-and-platforms/SKILL.md

Metadata

Files
0
Version
aaabf90
Hash
d747eecd
Indexed
2026-09-22 03:38

Accueil - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-09-22 09:59
浙ICP备14020137号-1