- 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.
```dart
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
```dart
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
```dart
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
```dart
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
```dart
@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
```dart
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.
在 GitHub 查看