| name | test-factories |
| description | Test factories and fixtures: factory_boy (Python), Fishery/faker-js (TypeScript), seeding test databases, avoiding test data duplication |
Test Factories Skill
When to activate
- Tests are slow or brittle because each test manually creates database records
- Copy-pasting model creation code across tests
- Setting up complex object graphs for a test (user → team → project → tasks)
- Seeding a database for development or staging
- Adding faker-based random data to test edge cases
When NOT to use
- Unit tests testing pure functions with no DB — just pass the data directly
- Very simple tests where one
db.create() call is clear and sufficient
- When the test data needs to be very specific and explicit (use literal values, not factories)
Instructions
Python — factory_boy
pip install factory-boy faker
import factory
from factory.django import DjangoModelFactory
from faker import Faker
fake = Faker()
class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = User
sqlalchemy_session_persistence = "commit"
email = factory.LazyFunction(fake.email)
name = factory.LazyFunction(fake.name)
role = "user"
is_active = True
class TeamFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = Team
name = factory.LazyFunction(lambda: fake.company())
owner = factory.SubFactory(UserFactory)
class ProjectFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = Project
name = factory.Sequence(lambda n: f"Project {n}")
team = factory.SubFactory(TeamFactory)
class TaskFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = Task
title = factory.LazyFunction(fake.sentence)
status = factory.Iterator(["todo", "in_progress", "done"])
assignee = factory.SubFactory(UserFactory)
project = factory.SubFactory(ProjectFactory)
def test_active_tasks():
task = TaskFactory(status="in_progress")
admin = UserFactory(role="admin", email="admin@example.com")
tasks = TaskFactory.create_batch(10, project=project)
user = UserFactory.build()
class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = User
class Params:
admin = factory.Trait(role="admin", is_staff=True)
inactive = factory.Trait(is_active=False)
admin = UserFactory(admin=True)
suspended = UserFactory(inactive=True)
TypeScript — Fishery + faker
npm install --save-dev fishery @faker-js/faker
import { Factory } from 'fishery'
import { faker } from '@faker-js/faker'
const userFactory = Factory.define<User>(() => ({
id: faker.string.uuid(),
email: faker.internet.email(),
name: faker.person.fullName(),
role: 'user' as const,
createdAt: faker.date.past(),
isActive: true,
}))
const teamFactory = Factory.define<Team>(({ associations }) => ({
id: faker.string.uuid(),
name: faker.company.name(),
ownerId: associations.owner?.id ?? faker.string.uuid(),
}))
const projectFactory = Factory.define<>( ({
: faker..(),
: ,
: faker..(),
}))
{ userFactory, teamFactory, projectFactory }
import { userFactory, projectFactory } from './factories'
describe('ProjectService', () => {
it('lists only active projects', () => {
const activeProject = projectFactory.build()
const archivedProject = projectFactory.build({ archived: true })
const projects = projectFactory.buildList(5)
const adminUser = userFactory.build({ role: 'admin', email: 'admin@test.com' })
})
})
const taskFactory = Factory.define<Task, { db: PrismaClient }>(
({ transientParams }) => ({
id: faker.string.uuid(),
title: faker.lorem.sentence(),
status: 'todo',
userId: faker.string.uuid(),
})
).withDecorator(async (task, { : { db } }) => {
db..({ : task })
})
task = taskFactory.({ : prisma })
Pytest fixtures with factories
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
from tests.factories import UserFactory, ProjectFactory, TaskFactory
@pytest.fixture
def db_session(engine):
UserFactory._meta.sqlalchemy_session = session
ProjectFactory._meta.sqlalchemy_session = session
yield session
session.rollback()
@pytest.fixture
def user(db_session):
return UserFactory()
@pytest.fixture
def admin(db_session):
return UserFactory(role="admin")
@pytest.fixture
def project_with_tasks(db_session, user):
project = ProjectFactory(owner=user)
TaskFactory.create_batch(5, project=project, assignee=user)
return project
def test_can_archive_project(project_with_tasks, admin):
result = archive_project(project_with_tasks.id, by=admin)
assert result.archived is True
Jest/Vitest fixtures pattern
import { PrismaClient } from '@prisma/client'
import { userFactory, projectFactory } from './factories'
export async function createUserWithProjects(
db: PrismaClient,
projectCount = 3
) {
const user = await db.user.create({
data: {
...userFactory.build(),
projects: {
create: projectFactory.buildList(projectCount),
},
},
include: { projects: true },
})
return user
}
export async function withTestUser(
db: PrismaClient,
fn: (user: User) => Promise<void>
) {
const user = await db.user.create({ data: userFactory.build() })
try {
await fn(user)
} finally {
await db..({ : { : user. } })
}
}
Database seeding
import asyncio
from tests.factories import UserFactory, TeamFactory, ProjectFactory, TaskFactory
async def seed():
users = UserFactory.create_batch(10)
admin = UserFactory(role="admin", email="admin@example.com", name="Admin User")
for i in range(3):
team = TeamFactory(owner=users[i])
for j in range(5):
project = ProjectFactory(team=team)
TaskFactory.create_batch(10, project=project)
print(f"Seeded: {len(users) + 1} users, 3 teams, 15 projects, 150 tasks")
asyncio.run(seed())
import { PrismaClient } from '@prisma/client'
import { userFactory, projectFactory, taskFactory } from '../tests/factories'
const prisma = new PrismaClient()
async function seed() {
await prisma.$transaction(async (tx) => {
const users = await Promise.all(
userFactory.buildList(10).map(u => tx.user.create({ data: u }))
)
for (const user of users) {
const projects = await Promise.all(
projectFactory.buildList(3, { userId: user.id })
.map(p => tx.project.create({ data: p }))
)
for (const project projects) {
tx..({
: taskFactory.(, { : project. }),
})
}
}
})
.()
}
().( prisma.$disconnect())
npx prisma db seed
python scripts/seed.py
Rules for good factories
- Sane defaults — every field should have a sensible value without any override
- Unique where required — use
factory.Sequence or faker for unique fields
- No hard-coded IDs — use generated UUIDs
- Build without DB by default — use
.build() for unit tests, .create() only for integration tests
- Traits for named variations —
UserFactory(admin=True) not AdminUserFactory()
- Don't embed business logic — factories are for data, not behaviour
Example
User: A test for an order service needs: 1 user, 1 cart with 3 items, a discount code, and a payment method on file — all linked correctly.
Expected setup using factories:
def test_checkout_applies_discount(db_session):
user = UserFactory()
payment = PaymentMethodFactory(user=user, type="card")
coupon = CouponFactory(code="SAVE20", discount_pct=20, active=True)
cart = CartFactory(user=user)
items = CartItemFactory.create_batch(3, cart=cart)
result = checkout(
user_id=user.id,
coupon_code="SAVE20",
payment_method_id=payment.id,
)
expected_total = sum(i.price * i.qty for i in items) * 0.8
assert result.total == pytest.approx(expected_total)