3 min read
Test data that tells the truth
Fixtures lie by omission. mongoose-test-factory reads your schema and generates data that respects it. How it decides what to generate, and the three modes I reach for every day.
Every Mongoose project I have joined had a fixtures/ folder, and every one of those folders was quietly wrong. A field was added to the schema and not to the JSON. An enum gained a value nobody tested. A required flag appeared and half the fixtures would no longer save, so someone marked the test skip and moved on.
Fixtures are a snapshot of what the schema looked like the day they were written. The schema is what the code believes today. mongoose-test-factory exists to close that gap: it reads the schema and generates data that is valid against it, right now.
#Start from the schema
The package is a plugin. Apply it, wrap the model, and the model gets a factory().
import mongoose, { Schema } from 'mongoose'
import mongooseTestFactory, { withFactory } from 'mongoose-test-factory'
const userSchema = new Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
age: { type: Number, min: 18, max: 120 },
isActive: { type: Boolean, default: true },
})
userSchema.plugin(mongooseTestFactory)
export const User = withFactory(mongoose.model('User', userSchema))const user = User.factory().build()
// { name: 'John Doe', email: 'john.doe@example.com', age: 28, isActive: true }Nothing was configured. The plugin walked the schema, saw a required string called name, a unique string called email, a number bounded between 18 and 120, and generated accordingly. Change the schema and the next run changes with it.
#How it decides what to generate
Three layers, in order of priority.
- An explicit
factoryTypeon the field wins. Forty-odd types are built in:email,phone,price,slug,uuid,birthdate,tags, and so on. - The field name.
userEmail,contactEmailandemailall look like emails.price,costandamountlook like money.createdAtlooks like a timestamp. - The type and its validators. A
Numberwithminandmaxstays inside them. AStringwith anenumpicks from it.requiredis always honoured.
When the name is ambiguous, say so in the schema:
const productSchema = new Schema({
name: { type: String, factoryType: 'title' },
vendor: { type: String, factoryType: 'company' },
price: { type: Number, factoryType: 'price' },
isActive: { type: Boolean, factoryType: 'active' }, // true about 80% of the time
website: { type: String, factoryType: 'url' },
})#Three modes, three kinds of test
| Method | Returns | Hits the database | Use it for |
|---|---|---|---|
build() | plain objects | no | unit tests, request bodies |
make() | Mongoose instances | no | virtuals, methods, hooks |
create() | saved documents | yes | integration tests |
const body = User.factory().build() // fast, no connection needed
const doc = User.factory().make() // an instance, not persisted
const saved = await User.factory(50).create() // fifty real rowsMost of my unit tests never open a connection. build() is fast enough that I stopped caching test data entirely.
#Overrides and relations
The generated value is a starting point. Whatever the test actually cares about, set explicitly.
const admin = User.factory().with({ name: 'Admin', role: 'admin' }).build()
const author = await User.factory().create()
const posts = await Post.factory(5).with({ author: author._id }).create()That second example is the pattern I use most: one real parent, N generated children pointing at it. The factory fills in everything I did not mention, and the assertions read as intent rather than setup.
#What it will not do
It will not invent your business rules. If an order's total must equal the sum of its lines, the schema does not say so, and the factory cannot know. Write that as an override, or as a small helper in your test setup that builds consistent orders. Generated data should be valid against the schema and honest about the rest; pretending it understands the domain would be another kind of lie.
Did this land?
Conversation
Building something like this?
Tell me what you are working on. I reply within a day.