test-fixture-factory
Version:
A minimal library for creating and managing test fixtures using Vitest, enabling structured, repeatable, and efficient testing processes.
288 lines (207 loc) • 8.63 kB
Markdown
# test-fixture-factory
`test-fixture-factory` is an NPM package designed to streamline the creation and management of test fixtures within TypeScript projects using **Vitest** [Test Contexts](https://vitest.dev/guide/test-context.html). This library leverages structured factory functions to generate test data and manage the lifecycle of these fixtures efficiently, making your tests more organized, repeatable, and maintainable.
## Table of Contents
- [Installation](#installation)
- [Features](#features)
- [Why Use test-fixture-factory?](#why-use-test-fixture-factory)
- [Getting Started](#getting-started)
- [Usage](#usage)
- [Defining a Factory](#defining-a-factory)
- [Using Factories in Tests](#using-factories-in-tests)
- [Destroying Resources](#destroying-resources)
- [API](#api)
- [License](#license)
## Installation
To add `test-fixture-factory` to your project, run:
```bash
npm add --save-dev test-fixture-factory
```
Ensure you have `vitest` set up as your test runner since this package is designed to work with it.
## Features
- **Define Factories:** Easily define factories for creating fixtures with dependencies and attributes.
- **Lifecycle Management:** Automatically manage the creation and destruction of test resources.
- **Integration with Vitest:** Seamlessly integrates with Vitest's testing functions.
Here’s a brief example illustrating a feature:
```typescript
import { defineFactory } from 'test-fixture-factory';
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 } });
},
};
});
```
## Why Use test-fixture-factory?
Testing with accurate and meaningful data is crucial for ensuring that your code behaves as expected. `test-fixture-factory` simplifies the process of setting up data for tests by providing:
- **Simplicity:** Avoid boilerplate code and manually setting up test data.
- **Consistency:** Ensure that each test runs with predictable and manageable data setup.
- **Isolation:** Prevent test case interference by cleaning up after tests automatically.
- **Robustness:** Handle complex dependencies among fixtures with ease.
## Getting Started
### Requirements
- [Node.js (v20 or later)](https://nodejs.org/)
- [Vitest](https://vitest.dev/) (may also work with [Playwright](https://playwright.dev/))
### Setup
1. Install the package using npm:
```bash
npm install test-fixture-factory
```
2. Ensure `vitest` is installed and configured in your project.
## Usage
### Defining a Factory
To define a factory, use the `defineFactory` function. A factory is a function that takes dependencies and attributes to produce a test value.
#### Without any Dependencies
```typescript
import { defineFactory } from 'test-fixture-factory';
import type { Company } from 'prisma'
type Dependencies = {}; // Alternatively as `Record<string, unknown>`
type Attributes = {
name: string
}
const companyFactory = defineFactory(
async ({}: Dependencies, attrs: Attributes): Company => {
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;
```
#### With Dependencies
```typescript
import { defineFactory } from 'test-fixture-factory';
import type { Company, User } from 'prisma'
type Dependencies = {
company: Company
}
type Attributes = {
name: string
email: string
}
const userFactory = defineFactory(
async ({ company }: Dependencies, attrs: Attributes): User => {
const user = await prisma.user.create({
company: company.id,
name: attrs.name,
email: attrs.email,
});
return {
value: user,
destroy: async () => {
await prisma.user.delete({ where: { id: user.id } });
}
};
});
export const useUser = userFactory.useValueFn;
export const useCreateUser = userFactory.useCreateFn;
```
### Using Factories in Tests
Factories can be used to create or directly retrieve values in test functions.
Dependencies such as `company` in the example below are automatically passed into factory functions
through Vitest's [Fixture Initialization](https://vitest.dev/guide/test-context.html#fixture-initialization).
```typescript
import { test as anyTest, expect } from 'vitest';
import { useCompany } from './factories/company.js'
import { useCreateUser } from './factories/user.js'
const test = anyTest.extend({
company: useCompany({ name: 'Crinkle' }),
createUser: useCreateUser()
});
test('it creates a user', async ({ company, createUser }) => {
const alice = await createUser({ name: 'Alice', email: 'alice@example.com' });
const bob = await createUser({ name: 'Bob', email: 'bob@example.com' });
expect(alice).toEqual({
id: expect.any(Number),
companyId: company.id,
name: 'Alice',
email: 'alice@example.com',
});
expect(bob).toEqual({
id: expect.any(Number),
companyId: company.id,
name: 'Bob',
email: 'bob@example.com',
});
/**
* Note: once this test has completed, alice and bob will both be removed
* from the database.
*/
});
```
### Reusing Fixtures
```typescript
import { test as anyTest, expect } from "vitest";
import { InferFixtureValue } from "test-fixture-factory";
import { useCompany } from "./factories/company.js";
import { useCreateUser } from "./factories/user.js";
const createFixtures = async ({
createUser,
}: {
createUser: InferFixtureValue<typeof useCreateUser>;
}) => {
const alice = await createUser({ name: "Alice", email: "alice@example.com" });
const bob = await createUser({ name: "Bob", email: "bob@example.com" });
return { alice, bob };
};
const test = anyTest.extend({
company: useCompany(),
createUser: useCreateUser(),
});
test("tests something", async ({ company, createUser }) => {
const { alice, bob } = await createFixtures({ createUser });
// ...
});
test("tests something else", async ({ company, createUser }) => {
const { alice, bob } = await createFixtures({ createUser });
// ...
});
```
### Destroying Resources
Factories ensure resources are destroyed properly after use. This avoids any residual data that might affect subsequent tests. Each factory can optionally specify a `destroy` function to clean up resources.
Since `destroy` is called in the reverse order of fixture definition, this should avoid any dependency conflicts (e.g. mandatory foreign key relationships in database tables).
### Resource Cleanup Control
There are two ways to control whether resources are automatically cleaned up after tests:
#### Environment Variable
You can set the `TFF_SKIP_DESTROY` environment variable to any non-empty value to globally disable resource cleanup:
```bash
TFF_SKIP_DESTROY=1 npm test
```
This is useful during development when you want to inspect the state of resources after tests run.
#### Per-Factory Options
You can also control cleanup behavior at the factory level using the `shouldDestroy` option:
```typescript
// Disable cleanup for a specific useValueFn call
const test = anyTest.extend({
company: useCompany({}, { shouldDestroy: false }),
createCompany: useCreateCompany({ shouldDestroy: false })
});
```
The `shouldDestroy` option:
- Defaults to `true` unless `TFF_SKIP_DESTROY` is set
- Can be passed to both `useValueFn` and `useCreateFn`
- Takes precedence over the `TFF_SKIP_DESTROY` environment variable
This granular control is useful when:
- You need to keep certain resources for inspection after specific tests
- You want to manage cleanup manually
- You're debugging specific test scenarios
## API
### `defineFactory(factoryFn)`
**Parameters:**
- `factoryFn`: A function that produces the fixture, taking dependencies and attributes, and returns an object containing the value and an optional `destroy` function.
**Returns:**
- The `defineFactory` function returns the same `factoryFn` that was passed in. However, this function now has extra methods available on it: `useCreateFn` and `useValueFn`.
- **`useCreateFn`**: Provides a function to create instances of the fixture with managed lifecycle.
- **`useValueFn(attrs)`**: Directly retrieves a fixture value, managing the lifecycle automatically.
## License
This package is [MIT licensed](LICENSE).