---
title: "Test data that tells the truth"
description: "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."
date: 2026-08-12
tags: ["mongodb", "testing", "typescript"]
language: en
canonical: https://aissamirhir.com/blog/test-data-that-tells-the-truth
source: aissamirhir.com
---
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](https://www.npmjs.com/package/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()`.

```typescript title="user.model.ts"
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))
```

```typescript
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.

1. **An explicit `factoryType`** on the field wins. Forty-odd types are built in: `email`, `phone`, `price`, `slug`, `uuid`, `birthdate`, `tags`, and so on.
2. **The field name.** `userEmail`, `contactEmail` and `email` all look like emails. `price`, `cost` and `amount` look like money. `createdAt` looks like a timestamp.
3. **The type and its validators.** A `Number` with `min` and `max` stays inside them. A `String` with an `enum` picks from it. `required` is always honoured.

When the name is ambiguous, say so in the schema:

```typescript
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' },
})
```

> [!TIP]
> Semantic field names pay twice. The factory reads them to pick a generator, and so does the next engineer who opens the file. `email` beats `str1` for both readers.

## 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 |

```typescript
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 rows
```

Most 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.

```typescript
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.
