Testing
Ormed provides testing utilities for isolated database tests.
import 'package:ormed/testing.dart';
Isolation Strategies
Configure the isolation strategy in setUpOrmed.
| Strategy | Description |
|---|---|
migrateWithTransactions | Default. Runs migrations once, wraps each test in a rolled-back transaction. |
truncate | Runs migrations once, truncates tables after each test. Use when testing transactions. |
recreate | Drops 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.
- Basic
- SQLite in-memory
- Seeders
- Real DB
- Migrations
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();
}
Future<void> inMemoryExecutorExample() async {
final dataSource = DataSource(
DataSourceOptions(
name: 'test',
driver: SqliteDriverAdapter.inMemory(),
entities: generatedOrmModelDefinitions,
),
);
await dataSource.init();
// The SQLite in-memory driver automatically:
// - Generates auto-increment IDs
// - Enforces foreign keys (when enabled by the driver)
// - Starts from a fresh database per DataSource instance
// - Supports basic query operations
}
class UserSeeder extends Seeder {
Future<void> run(DataSource dataSource) async {
final users = [
$User(id: 0, name: 'Admin', email: 'admin@example.com'),
$User(id: 0, name: 'User', email: 'user@example.com'),
];
for (final user in users) {
await dataSource.repo<$User>().insert(user);
}
}
}
Future<void> seederExample() async {
final dataSource = DataSource(
DataSourceOptions(
name: 'test',
driver: SqliteDriverAdapter.inMemory(),
entities: generatedOrmModelDefinitions,
),
);
await dataSource.init();
final seeder = UserSeeder();
await seeder.run(dataSource);
}
Future<void> realDatabaseExample() async {
late DataSource dataSource;
late String testDbPath;
// setUp
testDbPath = 'test_${DateTime.now().millisecondsSinceEpoch}.db';
dataSource = DataSource(
DataSourceOptions(
name: 'integration_test',
driver: SqliteDriverAdapter.file(testDbPath),
entities: generatedOrmModelDefinitions,
),
);
await dataSource.init();
// test...
// tearDown
await dataSource.dispose();
final file = File(testDbPath);
if (await file.exists()) {
await file.delete();
}
}
// For migration-driven test setups with FK-safe cleanup:
// Use setUpOrmed from package:ormed/testing.dart
//
// setUpOrmed(
// dataSource: myDataSource,
// migrationDescriptors: [
// MigrationDescriptor.fromMigration(
// id: MigrationId(DateTime.utc(2024, 1, 1), 'create_users'),
// migration: CreateUsersTable(),
// ),
// ],
// seeders: [UserSeeder.new],
// strategy: DatabaseIsolationStrategy.migrateWithTransactions,
// parallel: true,
// );
//
// ormedTest('creates a user', () async {
// final user = $User(id: 0, name: 'Ada', email: 'ada@test.com');
// await dataSource.repo<$User>().insert(user);
// expect(user.id, isNotNull);
// });
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();
}
| Strategy | Speed | Use Case |
|---|---|---|
migrateWithTransactions | Fast | Default |
truncate | Moderate | Testing transaction behavior |
recreate | Slow | Schema changes, full isolation |
TestDatabaseManager
Access the underlying manager for lower-level control:
final manager = testDatabaseManager;
await manager?.seed([MySeeder()], ds);
Example Test Suite
- Setup
- Tests
- Tear Down
dataSource = DataSource(
DataSourceOptions(
name: 'test_${DateTime.now().microsecondsSinceEpoch}',
driver: SqliteDriverAdapter.inMemory(),
entities: generatedOrmModelDefinitions,
),
);
await dataSource.init();
// group('User model', () {
// test('can create user', () async {
final user = $User(id: 0, name: 'Test', email: 'test@example.com');
await dataSource.repo<$User>().insert(user);
// expect(user.id, isNotNull);
// });
// test('can find user by id', () async {
final found = await dataSource.query<$User>().find(user.id);
// expect(found?.email, equals('test@example.com'));
// });
// test('can update user', () async {
user.setAttribute('email', 'updated@example.com');
await dataSource.repo<$User>().update(user);
final updated = await dataSource.query<$User>().find(user.id);
// expect(updated?.email, equals('updated@example.com'));
// });
// test('can delete user', () async {
await dataSource.repo<$User>().delete(user);
final deleted = await dataSource.query<$User>().find(user.id);
// expect(deleted, isNull);
// });
// });
await dataSource.dispose();