File-Based Organization
For larger projects, organize fixtures in separate files for better maintainability and scalability. This guide shows a complete, production-ready example.
You can find the complete working example in the tests/fixtures/ directory of this repository.
Recommended Structure
tests/
├── fixtures/
│ ├── types.ts # Fixture type definitions
│ ├── env.fixture.ts # Config fixture (no dependencies)
│ ├── db.fixture.ts # Database fixture (depends on env)
│ ├── logger.fixture.ts # Logger fixture (depends on env)
│ ├── client.fixture.ts # Client fixture (depends on db, logger)
│ ├── registry.ts # Central registry
│ └── index.ts # Typed createFixtures export
└── example.test.ts # Example test file
Dependency Graph
env (root)
├── db
│ └── client
└── logger
└── client
When you request ["client"], the setup order is:
env(set up once, shared by db and logger)dbandlogger(sequentially, in dependency order)client
Cleanup happens in reverse order.
Running the Example
# Run all tests (includes the example)
bun test
# Or run just the example directly
bun test tests/example.test.ts
You’ll see console output showing:
- Fixture setup order
- Database queries
- Logger messages
- Cleanup in reverse order
Key Patterns Demonstrated
- Separate Files - Each fixture in its own file for better organization
- Dependency Resolution - Automatic setup based on dependencies
- Shared Dependencies -
envis only set up once even though bothdbandloggerdepend on it - Selective Fixtures - Request only what you need per test suite
- Cleanup Management - Automatic cleanup in reverse dependency order
Fixture Examples
Simple Fixture (No Dependencies)
// env.fixture.ts
import { defineFixture } from "fixtures-ts";
import type { Fixtures } from "./types";
export default defineFixture<Fixtures, [], Fixtures["env"]>([], async () => ({
value: {
apiUrl: "http://localhost:3000",
dbUrl: "postgresql://localhost/test",
logLevel: "info",
},
cleanup: async () => {
// No cleanup needed for config
},
}));
Fixture with Cleanup
// db.fixture.ts
import { defineFixture } from "fixtures-ts";
import type { Fixtures } from "./types";
export default defineFixture<Fixtures, ["env"], Fixtures["db"]>(
["env"],
async ({ env }) => {
const db = await createDatabase(env.dbUrl);
return {
value: db,
cleanup: async () => await db.destroy(),
};
},
);
Fixture with Multiple Dependencies
// client.fixture.ts
import { defineFixture } from "fixtures-ts";
import type { Fixtures } from "./types";
export default defineFixture<Fixtures, ["db", "logger"], Fixtures["client"]>(
["db", "logger"],
async ({ db, logger }) => {
logger.info("Initializing HTTP client");
return {
value: createTestClient(db, logger),
cleanup: async () => {
logger.info("Client cleanup");
},
};
},
);
Common Testing Patterns
Database with Test Data
// db.fixture.ts
import { defineFixture } from "fixtures-ts";
import type { Fixtures } from "./types";
export default defineFixture<Fixtures, ["env"], Fixtures["db"]>(
["env"],
async ({ env }) => {
const db = await createTestDatabase(env.dbUrl);
await db.schema.createTable("users").execute();
return {
value: db,
cleanup: async () => await db.destroy(),
};
},
);
// testUsers.fixture.ts
export default defineFixture<Fixtures, ["db"], Fixtures["testUsers"]>(
["db"],
async ({ db }) => {
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
await db.insertInto("users").values(users).execute();
return {
value: users,
cleanup: async () => await db.deleteFrom("users").execute(),
};
},
);
HTTP Server and Client
// server.fixture.ts
import { defineFixture } from "fixtures-ts";
import type { Fixtures } from "./types";
export default defineFixture<Fixtures, [], Fixtures["server"]>([], async () => {
const server = await startTestServer();
return {
value: server,
cleanup: async () => await server.close(),
};
});
// client.fixture.ts
export default defineFixture<Fixtures, ["server"], Fixtures["client"]>(
["server"],
async ({ server }) => ({
value: createClient({
baseUrl: server.url,
headers: { Authorization: "Bearer test-token" },
}),
cleanup: async () => {},
}),
);
MSW for API Mocking
// msw.fixture.ts
import { defineFixture } from "fixtures-ts";
import type { Fixtures } from "./types";
import { setupServer } from "msw/node";
import { handlers } from "../mocks/handlers";
export default defineFixture<Fixtures, [], Fixtures["msw"]>([], async () => {
const server = setupServer(...handlers);
server.listen({ onUnhandledRequest: "error" });
return {
value: server,
cleanup: async () => server.close(),
};
});
Logger
// logger.fixture.ts
import { defineFixture } from "fixtures-ts";
import type { Fixtures } from "./types";
export default defineFixture<Fixtures, ["env"], Fixtures["logger"]>(
["env"],
async ({ env }) => {
const logger = createLogger({
level: env.logLevel,
format: "json",
});
return {
value: logger,
cleanup: async () => {
// Flush logs if needed
},
};
},
);
Dependency Resolution
When you request a fixture, all dependencies are automatically resolved:
// Dependency chain:
// client.fixture.ts depends on db
// db.fixture.ts depends on env
// env.fixture.ts has no dependencies
const fixtures = createFixtures(["client"]);
await fixtures.setup();
// Sets up: env → db → client
Shared dependencies are set up only once:
// Both logger and db depend on env
// client depends on both logger and db
const fixtures = createFixtures(["client"]);
await fixtures.setup();
// Sets up: env (once) → logger → db → client
// (or env → db → logger → client, depending on topological sort)
Adapting to Your Project
- Update types.ts - Define your actual fixture types
- Create fixture files - One file per fixture (db, cache, server, etc.)
- Update registry.ts - Import and register all your fixtures
- Use in tests - Import
createFixturesand request what you need