test-fixture-factory
Version:
A minimal library for creating and managing test fixtures using Vitest, enabling structured, repeatable, and efficient testing processes.
283 lines (217 loc) • 8.04 kB
Markdown
# Migration Guide: v1 to v2
This is a **practical, step-by-step guide** for migrating your existing test-fixture-factory v1 code to v2.
> 📋 For a high-level overview of what changed and why, see [CHANGELOG.md](./CHANGELOG.md).
This guide focuses on the **how**—with concrete examples and fixes for common migration issues.
## Overview of Changes
### Key Differences
| Aspect | v1 | v2 |
|-------------------|----------------------------|--------------------------------------------------------------------|
| Entry point | `defineFactory(fn)` | `createFactory(name)` |
| API style | Single function | Fluent builder API |
| Schema definition | Function parameters | `.withSchema((f) => ({ ... }))` with field builders |
| Dependencies | Function parameters | `.withContext<T>()` + **`.from(...)` / `.maybeFrom(...)`** |
| Method names | `useValueFn`, `useCreateFn`| **`useValue`, `useCreateValue`** |
| Build signature | `build(context, attrs)` | **`build(attrs?, context?)`** |
> Note: v2 **does not** support `.dependsOn(...)` or `.optionalDefault(...)`. Use `.from(...)` / `.maybeFrom(...)`, `.optional()`, and pure `.default(...)` instead.
---
## Step-by-Step Migration
### Step 1: Update Imports
**Before (v1):**
```ts
import { defineFactory } from 'test-fixture-factory';
````
**After (v2):**
```ts
import { createFactory } from 'test-fixture-factory';
```
---
### Step 2: Simple Factory (No Dependencies)
**Before (v1):**
```ts
const companyFactory = defineFactory(async ({}, attrs: { name: string }) => {
const company = await prisma.company.create({ name: attrs.name });
return {
value: company,
destroy: async () => await prisma.company.delete({ where: { id: company.id } })
};
});
export const useCompany = companyFactory.useValueFn;
export const useCreateCompany = companyFactory.useCreateFn;
```
**After (v2):**
```ts
const companyFactory = createFactory('Company')
.withSchema((f) => ({
name: f.type<string>(),
}))
.withValue(async ({ name }) => {
const company = await prisma.company.create({ data: { name } });
return {
value: company,
destroy: () => prisma.company.delete({ where: { id: company.id } }),
};
});
export const { useValue: useCompany, useCreateValue: useCreateCompany } = companyFactory;
```
---
### Step 3: Factories with Dependencies
In v1 you received dependencies as the **first** parameter. In v2 you **declare** how fields read from context via `.from(...)` / `.maybeFrom(...)`.
**Before (v1):**
```ts
const userFactory = defineFactory(
async ({ company }, attrs: { name: string; email: string }) => {
const user = await prisma.user.create({
companyId: company.id,
name: attrs.name,
email: attrs.email,
});
return { value: user, destroy: async () => prisma.user.delete({ where: { id: user.id } }) };
}
);
```
**After (v2):**
```ts
type Company = { id: number };
const userFactory = createFactory('User')
.withContext<{ company: Company }>()
.withSchema((f) => ({
companyId: f.type<number>().from('company', ({ company }) => company.id),
name: f.type<string>(),
email: f.type<string>(),
}))
.withValue(async ({ companyId, name, email }) => {
const user = await prisma.user.create({ data: { companyId, name, email } });
return { value: user, destroy: () => prisma.user.delete({ where: { id: user.id } }) };
});
```
> Use `.from('key')` **without** a transform only when `Context['key']` already matches the field type. Otherwise, provide a transform `(ctx) => T`.
---
### Step 4: Default Values
Defaults are **pure** in v2 (no access to context or other fields). Use `.default(value | () => value)`.
**Before (v1):**
```ts
const productFactory = defineFactory(async ({}, attrs: {
name: string; price?: number; active?: boolean
}) => {
const product = await prisma.product.create({
name: attrs.name,
price: attrs.price ?? 99.99,
active: attrs.active ?? true,
});
return { value: product };
});
```
**After (v2):**
```ts
const productFactory = createFactory('Product')
.withSchema((f) => ({
name: f.type<string>(),
price: f.type<number>().default(99.99),
active: f.type<boolean>().default(true),
}))
.withValue(async ({ name, price, active }) => {
const product = await prisma.product.create({ data: { name, price, active } });
return { value: product };
});
```
## Advanced Migration Patterns
### Complex Dependencies
**Before (v1):**
```ts
const orderFactory = defineFactory(
async ({ company, user }, attrs: { amount: number; productId?: number }) => {
const order = await prisma.order.create({
companyId: company.id,
userId: user.id,
amount: attrs.amount,
productId: attrs.productId,
});
return { value: order };
}
);
```
**After (v2):**
```ts
type Company = { id: number };
type User = { id: number };
type Product = { id: number };
const orderFactory = createFactory('Order')
.withContext<{ company: Company; user: User; product?: Product }>()
.withSchema((f) => ({
companyId: f.type<number>().from('company', ({ company }) => company.id),
userId: f.type<number>().from('user', ({ user }) => user.id),
amount: f.type<number>(),
// Optional overall; try context, otherwise allow missing
productId: f
.type<number>()
.maybeFrom('product', ({ product }) => product?.id)
.optional(),
}))
.withValue(async ({ companyId, userId, amount, productId }) => {
const order = await prisma.order.create({
data: { companyId, userId, amount, productId },
});
return { value: order };
});
```
### Dynamic “Defaults” Derived from Other Attributes
In v1 you might compute a default based on another **attribute**. In v2, defaults are pure. Do one of:
* Mark the field **optional** and compute inside `.withValue(...)`, or
* Require it and let callers pass it.
**Before (v1):**
```ts
const accountFactory = defineFactory(async ({}, attrs: {
email: string; username?: string;
}) => {
const username = attrs.username ?? attrs.email.split('@')[0];
const account = await createAccount({ email: attrs.email, username });
return { value: account };
});
```
**After (v2):**
```ts
const accountFactory = createFactory('Account')
.withSchema((f) => ({
email: f.type<string>(),
username: f.type<string>().optional(), // optional in input
}))
.withValue(async ({ email, username }) => {
const finalUsername = username ?? email.split('@')[0];
const account = await createAccount({ email, username: finalUsername });
return { value: account };
});
```
---
## Troubleshooting
### Undefined Field Errors
**Error:**
```
[User] 1 required field(s) have undefined values:
- companyId: must be provided as an attribute or via the test context (company)
```
**Fix:** Provide the field as an attribute **or** ensure the fixture is on the test context and the field reads it via `.from(...)`.
```ts
// Option 1: Provide as attribute
const user = await createUser({ companyId: 123, name: 'John', email: 'j@x.com' });
// Option 2: Ensure fixture exists and the schema reads it
const test = anyTest.extend({
company: useCompany({ name: 'Acme' }),
createUser: useCreateUser(),
});
```
---
### Errors with Dependencies
**Error:**
```
Property 'company' does not exist on type '{}'
```
**Fix:** Declare context shape using `.withContext()` and read via `.from(...)`.
```ts
const userFactory = createFactory('User')
.withContext<{ company: Company }>()
.withSchema((f) => ({
companyId: f.type<number>().from('company', ({ company }) => company.id),
name: f.type<string>(),
email: f.type<string>(),
}));
```