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

// Test transaction rollback and concurrent access

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

// Test eager loading, lazy loading, and N+1 prevention

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

// Test constraint violations and error handling

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

// Test foreign key cascades and integrity constraints

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

// Different cleanup approaches for test isolation

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