Skip to main content

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() or afterCreating() 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

MethodDescription
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:

Runs after make() (sync).

  // Run code after make()
Model.factory<$User>().afterMaking((user) {
print('Created user: ${user.email}');
}).make();

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:

TypeGenerated Value
intRandom 1-1000
double, numRandom 0-1000.0
boolRandom true/false
String"ModelName_fieldName_XXXX"
DateTimeNow + random seconds (0-86400)
Carbon, CarbonInterfaceCarbon.now() + random seconds (0-86400)
Map<K,V>Empty map {}
List<T>Empty list []
Nullable types50% 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:

Use custom providers when a field needs deterministic or domain-specific behavior (hashing, UUIDs, time, etc.).

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