Model Factories
Model factories provide a convenient way to generate test data for your ORM models with deterministic seeding, field overrides, and persistence integration.
Quick Start
Future<void> factoryQuickStart(QueryContext context) async {
// Generate test data
final userData = Model.factory<$User>().values();
print(userData); // {'id': 42, 'email': 'User_email_1234', ...}
// Create a model instance (not persisted)
final user = Model.factory<$User>().make();
// Create and persist
final savedUser = await Model.factory<$User>().create(context: context);
}
Enabling Factory Support
Add ModelFactoryCapable mixin to your model:
(table: 'users')
class FactoryUser extends Model<FactoryUser> with ModelFactoryCapable {
const FactoryUser({required this.id, required this.email, this.name});
(isPrimaryKey: true, autoIncrement: true)
final int id;
final String email;
final String? name;
}
The generator detects ModelFactoryCapable anywhere in the inheritance chain.
Inheritance Support
// Base class with factory support
(table: 'base_items')
class BaseItem extends Model<BaseItem> with ModelFactoryCapable {
const BaseItem({required this.id, this.name});
(isPrimaryKey: true)
final int id;
final String? name;
}
// Derived class automatically gets factory support
(table: 'special_items')
class SpecialItem extends BaseItem {
const SpecialItem({required super.id, super.name, this.tags});
final List<String>? tags;
}
External Factory Classes
Define factories in their own class to keep defaults, states, and hooks in a single place:
class FactoryUserFactory extends ModelFactoryDefinition<FactoryUser> {
const FactoryUserFactory();
Map<String, Object?> defaults() => {
'email': 'factory@example.com',
'name': 'Factory User',
};
Map<String, StateTransformer<FactoryUser>> get states => {
'admin': _adminState,
};
static Map<String, Object?> _adminState(Map<String, Object?> attributes) => {
'name': 'Admin User',
};
}
When to Use External Factories
Use external factories when:
- Reusing across tests: The factory is used in multiple test files
- Complex defaults: The factory has many fields or complex initialization logic
- Multiple variants: You need several named states (admin, pending, completed, etc.)
- Factory hooks: You need
afterMaking()orafterCreating()callbacks - Project-wide consistency: You want standardized defaults across your test suite
Register the external factory before using it:
void registerExternalFactories() {
registerOrmFactories();
ModelFactoryRegistry.registerFactory<FactoryUser>(const FactoryUserFactory());
}
Then use Model.factory() as usual:
void useExternalFactory() {
final user = Model.factory<FactoryUser>().make();
}
Named states can be applied through the factory definition:
void useExternalFactoryState() {
final admin = ModelFactoryRegistry.externalFactoryFor<FactoryUser>()!
.stateNamed('admin')
.make();
}
Advanced External Factory Patterns
Factory Hooks and Configuration
Configure callbacks directly in your factory definition using the configure() method:
class UserFactoryWithHooks extends ModelFactoryDefinition<FactoryUser> {
const UserFactoryWithHooks();
Map<String, Object?> defaults() => {
'email': 'user@example.com',
'name': 'Test User',
};
void configure(ModelFactoryBuilder<FactoryUser> builder) {
// Setup callbacks for all instances created with this factory
builder
.afterMaking((user) {
// Normalize email to lowercase
user.setAttribute(
'email',
user.getAttribute('email').toString().toLowerCase(),
);
})
.afterCreating((user) async {
// Could send welcome email, setup profile, etc.
print('User created with ID: ${user.id}');
});
}
}
This keeps all factory logic in one place, making it easier to understand and maintain factory behavior.
Multiple States and Variants
Define named states for different model variations:
class FactoryUserWithStates extends ModelFactoryDefinition<FactoryUser> {
const FactoryUserWithStates();
Map<String, Object?> defaults() => {
'email': 'pending@example.com',
'name': 'Pending User',
};
Map<String, StateTransformer<FactoryUser>> get states => {
'pending': (attrs) => {
'email': 'pending@example.com',
'name': 'Pending User',
},
'active': (attrs) => {'email': 'active@example.com', 'name': 'Active User'},
'suspended': (attrs) => {
'email': 'suspended@example.com',
'name': 'Suspended User',
},
};
}
void useMultipleStates() {
// final pendingUser = ModelFactoryRegistry.externalFactoryFor<FactoryUser>()!
// .stateNamed('pending')
// .make();
//
// final activeUser = ModelFactoryRegistry.externalFactoryFor<FactoryUser>()!
// .stateNamed('active')
// .make();
}
This is more maintainable than passing complex override maps repeatedly in tests.
Factory Composition and Inheritance
Create specialized factories that inherit from base factories:
class AdminUserFactory extends ModelFactoryDefinition<FactoryUser> {
const AdminUserFactory();
Map<String, Object?> defaults() => {
...const FactoryUserFactory().defaults(),
'role': 'admin',
'active': true,
};
}
class InactiveUserFactory extends ModelFactoryDefinition<FactoryUser> {
const InactiveUserFactory();
Map<String, Object?> defaults() => {
...const FactoryUserFactory().defaults(),
'active': false,
};
}
void useComposedFactories() {
// final admin = Model.factory<FactoryUser>(
// definition: const AdminUserFactory(),
// ).make();
//
// final inactive = Model.factory<FactoryUser>(
// definition: const InactiveUserFactory(),
// ).make();
}
This prevents duplication and ensures consistency across related factory definitions.
Custom Field Generators
Override field generation logic for specific scenarios:
class EmailGeneratorFactory extends ModelFactoryDefinition<FactoryUser> {
const EmailGeneratorFactory();
Map<String, Object?> defaults() => {'name': 'Test User'};
ModelFactoryBuilder<FactoryUser> builder() {
return super.builder().withGenerator('email', (field, context) {
// Generate deterministic emails based on seed
final index = context.seed ?? 0;
return 'user_$index@test.example.com';
});
}
}
Project Organization
Keep factories organized and easy to discover:
// Recommended file structure:
// lib/
// factories/
// user_factory.dart
// admin_factory.dart
// post_factory.dart
// comment_factory.dart
// tests/
// factories_test.dart
//
// In test setUp:
// void setUp() {
// registerOrmFactories();
// ModelFactoryRegistry.registerFactory<User>(const UserFactory());
// ModelFactoryRegistry.registerFactory<Admin>(const AdminFactory());
// // ... etc
// }
Benefits:
- Consistency: All factories follow the same patterns
- Maintainability: Easy to update defaults across tests
- Discoverability: Related factories grouped together
- Isolation: Factories don't pollute model files
Factory Builder API
| Method | Description |
|---|---|
values() | Returns the generated column map |
value(field) | Returns a single generated value |
make({registry}) | Creates a model instance without persisting |
makeMany({registry}) | Creates multiple model instances without persisting |
create({context, returning}) | Creates and persists the model |
createMany({context, returning}) | Creates and persists multiple models |
withOverrides(map) | Sets multiple field overrides |
withField(field, value) | Sets a single field override |
withGenerator(field, fn) | Replaces the generator for a field |
seed(int) | Sets deterministic seed for reproducibility |
reset() | Clears generated values for fresh generation |
count(n) | Sets number of models to create with makeMany/createMany |
state(map) | Applies a state transformation |
stateUsing(fn) | Applies a closure-based state transformation |
sequence([...]) | Cycles attribute sets across batch creation |
sequenceUsing(fn) | Generates attributes based on index |
afterMaking(fn) | Registers callback after make() |
afterCreating(fn) | Registers callback after create() |
trashed([timestamp]) | Marks model as soft-deleted |
Field Overrides
Override specific fields while letting others be generated:
void fieldOverridesExample() {
final user = Model.factory<$User>().withOverrides({
'email': 'admin@example.com',
'role': 'admin',
}).make();
// Or override a single field
final user2 = Model.factory<$User>()
.withField('email', 'test@example.com')
.make();
}
Deterministic Seeding
Use seeds for reproducible test data:
void seedingExample() {
// Same seed = same output
final first = Model.factory<$User>().seed(42).values();
final second = Model.factory<$User>().seed(42).values();
assert(first['email'] == second['email']);
// Different seeds = different output
final third = Model.factory<$User>().seed(99).values();
assert(first['email'] != third['email']);
}
Batch Creation
Create multiple models at once using count():
Future<void> batchCreationExample(QueryContext context) async {
// Create 3 users without persisting
final users = Model.factory<$User>().count(3).makeMany();
// Create and persist 5 users
final savedUsers = await Model.factory<$User>()
.count(5)
.createMany(context: context);
// Combine with seed for reproducibility
final seededUsers = Model.factory<$User>().seed(42).count(10).makeMany();
}
State Transformations
Apply named state modifications to models:
void stateTransformationsExample() {
// Apply attribute overrides via state
final admin = Model.factory<$User>().state({
'role': 'admin',
'active': true,
}).make();
// Chain multiple states (applied in order)
final suspendedAdmin = Model.factory<$User>().state({'role': 'admin'}).state({
'suspended': true,
}).make();
// Use closure for computed states
final user = Model.factory<$User>()
.stateUsing(
(attrs) => {
'email': '${attrs['name']}@example.com'.toString().toLowerCase(),
},
)
.make();
}
Sequences
Cycle through attribute values when creating multiple models:
void sequenceExamples() {
// Alternate between roles
final users = Model.factory<$User>().count(4).sequence([
{'role': 'admin'},
{'role': 'user'},
]).makeMany();
// Results: admin, user, admin, user
// Use generator for index-based values
final indexedUsers = Model.factory<$User>()
.count(3)
.sequenceUsing((index) => {'email': 'user_$index@test.com'})
.makeMany();
// Results: user_0@..., user_1@..., user_2@...
}
Callbacks
Execute code after models are made or created:
- afterMaking
- afterCreating
- Chaining
Runs after make() (sync).
// Run code after make()
Model.factory<$User>().afterMaking((user) {
print('Created user: ${user.email}');
}).make();
Runs after create() (async).
// Run async code after create()
await Model.factory<$User>()
.afterCreating((user) async {
// await sendWelcomeEmail(user);
print('Persisted user: ${user.id}');
})
.create(context: context);
Callbacks can be chained and run in registration order.
// Chain multiple callbacks
await Model.factory<$User>()
.afterMaking((u) => print('Made: ${u.email}'))
.afterMaking((u) => print('Validated'))
.afterCreating((u) => print('Saved: ${u.id}'))
.create(context: context);
Soft-Deleted Models
Create models that are already soft-deleted:
void trashedExamples() {
// Create a trashed model (uses current timestamp)
final deletedUser = Model.factory<$User>().trashed().make();
// With custom deletion timestamp
final customTrashed = Model.factory<$User>()
.trashed(DateTime(2024, 1, 15))
.make();
}
Custom Field Generators
Replace the default generator for specific fields:
void customGeneratorExamples() {
final factory = Model.factory<$User>()
.withGenerator('email', (field, context) {
final suffix = context.random.nextInt(1000);
return 'user_$suffix@test.example.com';
})
.withGenerator('createdAt', (field, context) {
return DateTime(
2024,
1,
1,
).add(Duration(days: context.random.nextInt(365)));
});
final user = factory.make();
}
Carbon/CarbonInterface Fields
For models using Carbon or CarbonInterface fields, the factory automatically generates appropriate values:
void carbonFieldsExample() {
// Carbon fields are generated automatically
// final post = Model.factory<Post>().make();
// print(post.publishedAt); // Carbon instance within 24 hours of now
// Override with specific Carbon value
// final factory = Model.factory<Post>()
// .withGenerator('publishedAt', (field, context) {
// return Carbon.parse('2024-06-15 10:30:00');
// });
// Or use Carbon's fluent API
// final factory = Model.factory<Post>()
// .withGenerator('publishedAt', (field, context) {
// return Carbon.now().subDays(context.random.nextInt(30));
// });
}
Default Value Generation
The DefaultFieldGeneratorProvider generates values based on field types:
| Type | Generated Value |
|---|---|
int | Random 1-1000 |
double, num | Random 0-1000.0 |
bool | Random true/false |
String | "ModelName_fieldName_XXXX" |
DateTime | Now + random seconds (0-86400) |
Carbon, CarbonInterface | Carbon.now() + random seconds (0-86400) |
Map<K,V> | Empty map {} |
List<T> | Empty list [] |
| Nullable types | 50% chance of null |
Auto-increment fields and fields with defaultValueSql are skipped unless explicitly overridden.
Custom Generator Providers
Create a custom provider for specialized data generation:
- Notes
- Code
Use custom providers when a field needs deterministic or domain-specific behavior (hashing, UUIDs, time, etc.).
class FakerGeneratorProvider extends GeneratorProvider {
const FakerGeneratorProvider();
Object? generate<TModel extends OrmEntity>(
FieldDefinition field,
ModelFactoryGenerationContext<TModel> context,
) {
// Custom logic based on field name
if (field.name == 'email')
return 'faker_${context.random.nextInt(1000)}@test.com';
if (field.name == 'name') return 'User ${context.random.nextInt(100)}';
if (field.name == 'phone')
return '+1-555-${context.random.nextInt(10000).toString().padLeft(4, '0')}';
// Fall back to default
return const DefaultFieldGeneratorProvider().generate(field, context);
}
}
void customProviderExample() {
// Use the custom provider
final factory = Model.factory<$User>(
generatorProvider: const FakerGeneratorProvider(),
);
final user = factory.make();
}
Testing Patterns
Seeded Tests for Reproducibility
void seededTestExample() {
const testSeed = 12345;
final user = Model.factory<$User>().seed(testSeed).withOverrides({
'role': 'admin',
}).make();
// expect(processUser(user), expectedResult);
}
Factory Helpers for Common Scenarios
ModelFactoryBuilder<$User> adminUser() =>
Model.factory<$User>().withOverrides({'role': 'admin', 'active': true});
ModelFactoryBuilder<$User> inactiveUser() =>
Model.factory<$User>().withOverrides({'active': false});
// In tests:
void useFactoryHelpers() {
final admin = adminUser().make();
final inactive = inactiveUser().withField('email', 'test@example.com').make();
}
Cross-Model References
void crossModelReferencesExample() {
// Generate consistent foreign keys
final userId = Model.factory<$User>().seed(1).value('id') as int;
// final post = Model.factory<Post>()
// .withOverrides({'userId': userId, 'title': 'Test Post'})
// .make();
}
Batch Generation
Future<List<$User>> createUsers(QueryContext context, int count) async {
final users = <$User>[];
for (var i = 0; i < count; i++) {
final user = await Model.factory<$User>()
.seed(i)
.withField('email', 'user_$i@test.com')
.create(context: context);
users.add(user);
}
return users;
}
Connection-Bound Helpers
Use withConnection to get query and repository access:
void connectionBoundHelpersExample(QueryContext queryContext) {
// final helper = UserModelFactory.withConnection(queryContext);
// Query builder
// final users = await helper.query()
// .whereEquals('active', true)
// .get();
// Repository
// final repo = helper.repository();
// await repo.insert(user);
}
Best Practices
1. Keep Factories Simple
Factories should generate valid, realistic data. Complex logic belongs in application code, not factories:
// ❌ Avoid: Complex business logic
class OrderFactory extends ModelFactoryDefinition<Order> {
void configure(ModelFactoryBuilder<Order> builder) {
builder.afterCreating((order) async {
// Don't put business logic here
await calculateTax(order);
await checkInventory(order);
});
}
}
// ✅ Good: Just set realistic defaults
class OrderFactory extends ModelFactoryDefinition<Order> {
Map<String, Object?> defaults() => {
'status': 'pending',
'total': 100.0,
'tax': 15.0,
};
}
2. Use States for Variations
Instead of creating multiple factories, use states for variations of the same model:
// ❌ Avoid: Multiple nearly-identical factories
class PendingOrderFactory extends ModelFactoryDefinition<Order> {
Map<String, Object?> defaults() => {'status': 'pending'};
}
class CompletedOrderFactory extends ModelFactoryDefinition<Order> {
Map<String, Object?> defaults() => {'status': 'completed'};
}
// ✅ Good: One factory with multiple states
class OrderFactory extends ModelFactoryDefinition<Order> {
Map<String, Object?> defaults() => {'status': 'pending'};
Map<String, StateTransformer<Order>> get states => {
'completed': (attrs) => {'status': 'completed'},
'cancelled': (attrs) => {'status': 'cancelled'},
};
}
3. Preserve Referential Integrity
When creating models with relationships, ensure foreign keys are valid:
// ✅ Good: Create related models
await ormedTest('creates post with valid user_id', (fixture) async {
final user = await FactoryUser.factory().create();
final post = await FactoryPost.factory()
.withField('user_id', user.id)
.create();
expect(post.userId, equals(user.id));
});
4. Use Seeds for Debugging
When a test fails non-deterministically, seed the factory to reproduce it:
// First run: fails randomly
final user = FactoryUser.factory().make();
// Second run: fix the seed to reproduce the failure
final user = FactoryUser.factory().seed(12345).make();
5. Separate Fixture Setup
Extract repeated factory patterns into helper methods:
Future<(FactoryUser, List<FactoryPost>)> createUserWithPosts(int count) async {
final user = await FactoryUser.factory().create();
final posts = await FactoryPost.factory()
.withField('user_id', user.id)
.count(count)
.create();
return (user, posts);
}
// Usage in tests:
await ormedTest('user has 5 posts', (fixture) async {
final (user, posts) = await createUserWithPosts(5);
expect(user.posts, hasLength(5));
});
6. Override Only What Matters
Keep test intent clear by only overriding fields relevant to what you're testing:
// ❌ Unclear: Sets many fields unrelated to test
final user = await FactoryUser.factory()
.withField('name', 'John')
.withField('email', 'john@test.com')
.withField('status', 'active')
.withField('created_at', DateTime.now())
.create();
// ✅ Clear: Only override what the test cares about
final user = await FactoryUser.factory()
.withField('email', 'john@test.com')
.create();