Skip to main content

Testing

Ormed provides testing utilities for isolated database tests.

import 'package:ormed/testing.dart';

Isolation Strategies​

Configure the isolation strategy in setUpOrmed.

StrategyDescription
migrateWithTransactionsDefault. Runs migrations once, wraps each test in a rolled-back transaction.
truncateRuns migrations once, truncates tables after each test. Use when testing transactions.
recreateDrops and recreates the schema for every test. Slowest but complete isolation.

ormedGroup and ormedTest​

ormedGroup​

Groups related tests with a shared database.

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

ormedTest​

Individual test cases. Inherits the group's DataSource when nested, or creates a fresh database when standalone.

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. Use this instance for isolated test databases.

Access within setUp or tearDown:

setUp(() {
final ds = currentTestDataSource;
});

Core Setup​

Call setUpOrmed once in main() before defining tests.

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​

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​

Register all related entities:

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​

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
}

Factories​

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));
}

Transaction Testing​

Future<void> transactionTestingExample(DataSource dataSource) async {
// Test rollback on error
try {
await dataSource.transaction((tx) async {
final user = $User(id: 0, name: 'Test', email: 'test@example.com');
await tx.repo<$User>().insert(user);

// Simulate error
throw Exception('Rollback test');
});
} catch (e) {
// Transaction rolled back
}

// Verify rollback worked
final count = await dataSource.query<$User>().count();
// expect(count, equals(0));
}
Future<void> concurrentTransactionExample(DataSource dataSource) async {
// Test concurrent inserts
await Future.wait([
dataSource.transaction((tx) async {
await tx.repo<$User>().insert(
$User(id: 0, name: 'User1', email: 'user1@test.com'),
);
}),
dataSource.transaction((tx) async {
await tx.repo<$User>().insert(
$User(id: 0, name: 'User2', email: 'user2@test.com'),
);
}),
]);

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

Relationship Testing​

Future<void> eagerLoadingTest(DataSource dataSource) async {
final (user, _) = await createUserWithPosts(dataSource, 3);

// Eager load posts
final users = await dataSource.query<$User>().with_(['posts']).get();

// Access posts without additional query
final posts = users.first.posts;
// expect(posts?.length, equals(3));
}
Future<void> lazyLoadingTest(DataSource dataSource) async {
final (user, _) = await createUserWithPosts(dataSource, 2);

// Query without eager loading
final foundUser = await dataSource.query<$User>().find(user.id);

// Posts would need separate query (lazy loading)
final posts = await dataSource
.query<$Post>()
.whereEquals('author_id', user.id)
.get();
// expect(posts.length, equals(2));
}
Future<void> n1PreventionTest(DataSource dataSource) async {
// Create multiple users with posts
for (var i = 0; i < 3; i++) {
await createUserWithPosts(dataSource, 2);
}

// ❌ Bad: N+1 problem - 1 query for users + N queries for posts
// final users = await dataSource.query<$User>().get();
// for (final user in users) {
// final posts = await dataSource.query<$Post>()
// .whereEquals('author_id', user.id)
// .get();
// }

// ✅ Good: Single query with eager loading
final users = await dataSource.query<$User>().with_(['posts']).get();
// expect(users.length, equals(3));
// expect(users.first.posts?.length, equals(2));
}

Constraint Violations​

Future<void> uniqueConstraintTest(DataSource dataSource) async {
final user1 = $User(id: 0, name: 'Test', email: 'unique@test.com');
await dataSource.repo<$User>().insert(user1);

// Attempt duplicate email (should throw)
final user2 = $User(id: 0, name: 'Test2', email: 'unique@test.com');
try {
await dataSource.repo<$User>().insert(user2);
// Should not reach here
} catch (e) {
// expect(e, isA<DatabaseException>());
}
}
Future<void> foreignKeyConstraintTest(DataSource dataSource) async {
// Attempt to insert post with invalid user_id
final post = $Post(id: 0, title: 'Test', authorId: 9999);

try {
await dataSource.repo<$Post>().insert(post);
// Should not reach here if FK constraints enabled
} catch (e) {
// expect(e, isA<DatabaseException>());
}
}
Future<void> notNullConstraintTest(DataSource dataSource) async {
// Attempt to insert null into required field
try {
await dataSource.repo<$User>().insert($User(id: 0, name: '', email: ''));
} catch (e) {
// Validation or constraint error
}
}

Data Integrity​

Future<void> cascadeDeleteTest(DataSource dataSource) async {
final (user, posts) = await createUserWithPosts(dataSource, 3);

// Delete user (should cascade to posts if configured)
await dataSource.repo<$User>().delete(user);

// Verify cascade worked
final remainingPosts = await dataSource
.query<$Post>()
.whereEquals('author_id', user.id)
.get();
// expect(remainingPosts, isEmpty);
}
Future<void> referentialIntegrityTest(DataSource dataSource) async {
final user = $User(id: 0, name: 'Author', email: 'author@test.com');
await dataSource.repo<$User>().insert(user);

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

// Attempt to delete user with existing posts (should fail if no cascade)
try {
await dataSource.repo<$User>().delete(user);
// May throw if FK constraint prevents deletion
} catch (e) {
// expect(e, isA<DatabaseException>());
}
}

Cleanup Strategies​

Future<void> transactionRollbackCleanup(DataSource dataSource) async {
// Strategy 1: Transaction rollback (fastest)
// Use DatabaseIsolationStrategy.migrateWithTransactions in setUpOrmed
await dataSource.transaction((tx) async {
// All test operations in transaction
await tx.repo<$User>().insert(
$User(id: 0, name: 'Test', email: 'test@example.com'),
);
// Auto-rolled back after test
});
}
Future<void> truncateCleanup(DataSource dataSource) async {
// Strategy 2: Truncate tables (moderate speed)
// Use DatabaseIsolationStrategy.truncate in setUpOrmed
// Runs after each test to clear all data

// Manual truncate example:
await dataSource.execute('DELETE FROM users');
await dataSource.execute('DELETE FROM posts');
await dataSource.execute('DELETE FROM comments');
}
Future<void> recreateCleanup() async {
// Strategy 3: Recreate database (slowest, most thorough)
// Use DatabaseIsolationStrategy.recreate in setUpOrmed
// Drops and recreates entire schema for each test

late DataSource dataSource;
dataSource = DataSource(
DataSourceOptions(
name: 'test_${DateTime.now().microsecondsSinceEpoch}',
driver: SqliteDriverAdapter.inMemory(),
entities: generatedOrmModelDefinitions,
),
);
await dataSource.init();
// Complete isolation, but slower
await dataSource.dispose();
}
StrategySpeedUse Case
migrateWithTransactionsFastDefault
truncateModerateTesting transaction behavior
recreateSlowSchema changes, full isolation

TestDatabaseManager​

Access the underlying manager for lower-level control:

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();

Read This Next​