Skip to main content

Testing

Ormed provides comprehensive testing facilities for isolated, reliable database tests.

To get started, import the testing utilities:

import 'package:ormed/testing.dart';

Isolation Strategies

Ormed supports several strategies for isolating database state between tests. You can configure the strategy in setUpOrmed.

migrateWithTransactions (Default)

The fastest strategy. It runs migrations once per group and wraps each test in a transaction that is rolled back after the test completes.

truncate

Runs migrations once per group and truncates all tables after each test. This is useful for drivers that don't support nested transactions or when you need to test transaction behavior itself.

recreate

The most thorough but slowest strategy. It drops and recreates the entire schema/database for every test.

Group vs Test Isolation

Ormed provides two primary functions for defining tests: ormedGroup and ormedTest.

ormedGroup

Use ormedGroup to group related tests that share a common database setup. Every group gets its own unique database for true concurrency.

ormedGroup('User Management', (ds) {
// This database is provisioned once for the group
});

ormedTest

Use ormedTest for individual test cases.

  • When used inside an ormedGroup, it inherits the group's DataSource and isolation strategy.
  • When used standalone, it creates a completely fresh database for that specific test.
ormedTest('can create user', (ds) async {
final user = await ds.repo<User>().insert(User(name: 'Alice'));
expect(user.id, isNotNull);
});

DataSource Injection

Both ormedGroup and ormedTest inject a DataSource instance into their callbacks. Always use this injected instance to ensure your tests run against the isolated test database rather than a global or shared instance.

Accessing the Current DataSource

If you need to access the DataSource within standard setUp or tearDown blocks, you can use the currentTestDataSource getter:

setUp(() {
final ds = currentTestDataSource;
// Perform setup using the isolated data source
});

Core Setup

The setUpOrmed function is the entry point for configuring your test environment. It should be called once in your main() function before defining any tests.

Create a fresh DataSource for each test suite.

Future<void> basicTestSetup() async {
late DataSource dataSource;

// setUp
dataSource = DataSource(
DataSourceOptions(
name: 'test_db',
driver: SqliteDriverAdapter.inMemory(),
entities: generatedOrmModelDefinitions,
),
);
await dataSource.init();

// test
final user = $User(id: 0, name: 'Test', email: 'test@example.com');
await dataSource.repo<$User>().insert(user);

final found = await dataSource
.query<$User>()
.whereEquals('email', 'test@example.com')
.first();
print('Found: ${found?.name}');

// tearDown
await dataSource.dispose();
}

Static Helpers in Tests

Set a default DataSource for Model static helpers:

Future<void> staticHelpersExample() async {
final dataSource = DataSource(
DataSourceOptions(
name: 'test',
driver: SqliteDriverAdapter.inMemory(),
entities: generatedOrmModelDefinitions,
),
);
await dataSource.init();

// First DataSource initialized becomes default, or:
dataSource.setAsDefault();

// Now static helpers work
await Users.query().get();
}

Testing Relations

Ensure all related entities are registered:

Future<void> testingRelationsExample() async {
final dataSource = DataSource(
DataSourceOptions(
name: 'test',
driver: SqliteDriverAdapter.inMemory(),
entities: [
UserOrmDefinition.definition,
PostOrmDefinition.definition,
CommentOrmDefinition.definition,
],
),
);
await dataSource.init();

final user = $User(id: 0, name: 'Author', email: 'author@example.com');
await dataSource.repo<$User>().insert(user);

final post = $Post(id: 0, title: 'Test Post', authorId: user.id);
await dataSource.repo<$Post>().insert(post);

// Test eager loading
final users = await dataSource.query<$User>().with_(['posts']).get();
print('Posts count: ${users.first.posts?.length}');
}

Parallel Testing

For parallel test execution without conflicts:

Future<void> parallelTestingExample() async {
// Unique name per test suite
final dataSource = DataSource(
DataSourceOptions(
name: 'test_${DateTime.now().microsecondsSinceEpoch}',
driver: SqliteDriverAdapter.inMemory(),
entities: generatedOrmModelDefinitions,
),
);
await dataSource.init();
// Each test gets isolated database
}

Best Practices

Use SQLite In-Memory for Unit Tests

// Use SQLite in-memory for unit tests - fast and isolated
void inMemoryBestPractice() {
// driver: SqliteDriverAdapter.inMemory()
}

Use Real Databases for Integration Tests

// Use Real Databases for Integration Tests - More realistic
void realDbBestPractice() {
// driver: SqliteDriverAdapter.file('test.db')
}

Keep Tests Isolated

Future<void> keepTestsIsolated() async {
late DataSource dataSource;

// setUp - fresh database for each test
dataSource = DataSource(
DataSourceOptions(
name: 'test',
driver: SqliteDriverAdapter.inMemory(),
entities: generatedOrmModelDefinitions,
),
);
await dataSource.init();

// test 1 - This test's data won't affect test 2
// ...

// test 2 - Starts with clean database
// ...
}

Use Factories for Test Data

Future<void> useFactoriesForTestData(DataSource dataSource) async {
for (var i = 0; i < 100; i++) {
await Model.factory<User>()
.seed(i)
.withField('email', 'user_$i@test.com')
.create(context: dataSource.context);
}

final count = await dataSource.query<$User>().count();
// expect(count, equals(100));
}

Test Both Success and Failure Cases

Future<void> testBothSuccessAndFailure(DataSource dataSource) async {
// test success case
await dataSource.repo<$User>().insert(
$User(id: 0, name: 'Test', email: 'test@example.com'),
);

// test failure case - throws on duplicate email
// expect(
// () => dataSource.repo<$User>().insert(
// $User(id: 0, name: 'Test2', email: 'test@example.com'),
// ),
// throwsException,
// );
}

Advanced: TestDatabaseManager

TestDatabaseManager is the underlying engine that manages the lifecycle of test databases. While ormedGroup and ormedTest are the preferred high-level APIs, you can access the manager via the testDatabaseManager getter if you need lower-level control, such as manually seeding data or checking migration status.

final manager = testDatabaseManager;
await manager?.seed([MySeeder()], ds);

Example Test Suite

  dataSource = DataSource(
DataSourceOptions(
name: 'test_${DateTime.now().microsecondsSinceEpoch}',
driver: SqliteDriverAdapter.inMemory(),
entities: generatedOrmModelDefinitions,
),
);
await dataSource.init();