Test Helpers Overview
Fixture Loader
Test data reuse Type-safe loading
Fixture Manager
Copy data between DBs Auto relationship handling
Easy Setup
Define once Share across tests
Auto Cleanup
Transaction based Auto rollback
Fixture Loader
createFixtureLoader
Pre-define fixtures for tests and load when needed.// api/src/testing/fixture.ts
import { createFixtureLoader } from "sonamu/test";
import { UserModel } from "@/models/user.model";
import { PostModel } from "@/models/post.model";
export const loadFixtures = createFixtureLoader({
// User fixtures
user01: async () => {
const userModel = new UserModel();
const { user } = await userModel.create({
username: "john",
email: "john@example.com",
password: "password123",
});
return user;
},
user02: async () => {
const userModel = new UserModel();
const { user } = await userModel.create({
username: "jane",
email: "jane@example.com",
password: "password456",
});
return user;
},
admin: async () => {
const userModel = new UserModel();
const { user } = await userModel.create({
username: "admin",
email: "admin@example.com",
password: "admin123",
role: "admin",
});
return user;
},
// Post fixtures
post01: async () => {
const postModel = new PostModel();
const { post } = await postModel.create({
title: "First Post",
content: "Content of first post",
author_id: 1,
});
return post;
},
});
Using in Tests
// api/src/models/post.model.test.ts
import { bootstrap, test } from "sonamu/test";
import { expect, vi } from "vitest";
import { PostModel } from "./post.model";
import { loadFixtures } from "@/testing/fixture";
bootstrap(vi);
test("Get user's posts", async () => {
// Load fixtures
const { user01, post01 } = await loadFixtures(["user01", "post01"]);
// Test
const postModel = new PostModel();
const { posts } = await postModel.getPostsByUser(user01.id);
expect(posts).toHaveLength(1);
expect(posts[0].id).toBe(post01.id);
});
test("Admin permission test", async () => {
// Load only needed fixtures
const { admin } = await loadFixtures(["admin"]);
expect(admin.role).toBe("admin");
});
Benefits
Type safety:// β
Type safe: IDE provides auto-completion
const { user01 } = await loadFixtures(["user01"]);
user01.username; // string
// β Non-existent fixture
const { user999 } = await loadFixtures(["user999"]); // Type error!
// Use same fixture in multiple tests
test("Test 1", async () => {
const { user01 } = await loadFixtures(["user01"]);
// ...
});
test("Test 2", async () => {
const { user01 } = await loadFixtures(["user01"]);
// ...
});
// Load only what's needed
const { user01, user02 } = await loadFixtures(["user01", "user02"]);
// admin not loaded β Save creation cost
Complex Fixture Patterns
Fixtures with Relationships
// api/src/testing/fixture.ts
export const loadFixtures = createFixtureLoader({
author: async () => {
const userModel = new UserModel();
const { user } = await userModel.create({
username: "author",
email: "author@example.com",
password: "password",
});
return user;
},
authorPost: async () => {
// Load author fixture first
const { author } = await loadFixtures(["author"]);
const postModel = new PostModel();
const { post } = await postModel.create({
title: "Author's Post",
content: "Content",
author_id: author.id, // Set relationship
});
return post;
},
});
// Using in test
test("Author and post relationship", async () => {
const { author, authorPost } = await loadFixtures(["author", "authorPost"]);
expect(authorPost.author_id).toBe(author.id);
});
Dynamic Fixtures
// api/src/testing/fixture.ts
export const loadFixtures = createFixtureLoader({
// Fixture that takes parameters
userWithEmail: async (email: string) => {
const userModel = new UserModel();
const { user } = await userModel.create({
username: email.split("@")[0],
email,
password: "password",
});
return user;
},
// Create multiple data
users10: async () => {
const userModel = new UserModel();
const users = [];
for (let i = 1; i <= 10; i++) {
const { user } = await userModel.create({
username: `user${i}`,
email: `user${i}@example.com`,
password: "password",
});
users.push(user);
}
return users;
},
});
Fixture Manager
Overview
FixtureManager provides functionality to copy real data from production DB to test DB.
Key features:
- Extract data from Production β Fixture DB
- Sync Fixture DB β Test DB
- Auto relationship resolution (BelongsTo, HasMany, ManyToMany)
- Duplicate data handling
DB sync
Sync Fixture DB data to Test DB.// api/src/application/sonamu.ts
import { FixtureManager } from "sonamu/test";
// Fixture DB β Test DB sync
await FixtureManager.sync();
- Initialize Test DB (delete existing data)
- Fixture DB dump (
pg_dump) - Restore to Test DB (
pg_restore)
importFixture
Import specific data from Production DB to Fixture DB.import { FixtureManager } from "sonamu/test";
// Initialize
FixtureManager.init();
// Import User ID 1, 2, 3 to Fixture DB
await FixtureManager.importFixture("User", [1, 2, 3]);
// Auto import all related data (Posts, Comments, etc.)
// Even if only User ID 1 requested...
await FixtureManager.importFixture("User", [1]);
// Auto imports:
// - User #1
// - User #1's Profile (OneToOne)
// - User #1's Posts (HasMany)
// - Post's Comments (HasMany)
// - Comment's Author (BelongsTo)
Practical Examples
Basic Fixture Pattern
// api/src/testing/fixture.ts
import { createFixtureLoader } from "sonamu/test";
import { UserModel } from "@/models/user.model";
import { PostModel } from "@/models/post.model";
export const loadFixtures = createFixtureLoader({
// 1. Simple data
guestUser: async () => {
const userModel = new UserModel();
const { user } = await userModel.create({
username: "guest",
email: "guest@example.com",
password: "password",
role: "guest",
});
return user;
},
// 2. Multiple data
testUsers: async () => {
const userModel = new UserModel();
const users = await Promise.all([
userModel.create({
username: "user1",
email: "user1@example.com",
password: "password",
}),
userModel.create({
username: "user2",
email: "user2@example.com",
password: "password",
}),
]);
return users.map(({ user }) => user);
},
// 3. Data with relationships
userWithPosts: async () => {
const userModel = new UserModel();
const postModel = new PostModel();
const { user } = await userModel.create({
username: "blogger",
email: "blogger@example.com",
password: "password",
});
const posts = await Promise.all([
postModel.create({
title: "Post 1",
content: "Content 1",
author_id: user.id,
}),
postModel.create({
title: "Post 2",
content: "Content 2",
author_id: user.id,
}),
]);
return {
user,
posts: posts.map(({ post }) => post),
};
},
});
Complex Scenario
// api/src/models/comment.model.test.ts
import { bootstrap, test } from "sonamu/test";
import { expect, vi } from "vitest";
import { CommentModel } from "./comment.model";
import { loadFixtures } from "@/testing/fixture";
bootstrap(vi);
test("Create and retrieve comments", async () => {
// Load needed fixtures
const { userWithPosts } = await loadFixtures(["userWithPosts"]);
const { user, posts } = userWithPosts;
const commentModel = new CommentModel();
// Create comment
const { comment } = await commentModel.create({
content: "Great post!",
post_id: posts[0].id,
author_id: user.id,
});
// Get comments
const { comments } = await commentModel.getCommentsByPost(posts[0].id);
expect(comments).toHaveLength(1);
expect(comments[0].content).toBe("Great post!");
expect(comments[0].author_id).toBe(user.id);
});
Best Practices
1. Fixture Naming Convention
// β
Correct: Clear names
export const loadFixtures = createFixtureLoader({
adminUser: async () => {
/* ... */
},
guestUser: async () => {
/* ... */
},
publishedPost: async () => {
/* ... */
},
draftPost: async () => {
/* ... */
},
});
// β Wrong: Unclear names
export const loadFixtures = createFixtureLoader({
user1: async () => {
/* ... */
},
user2: async () => {
/* ... */
},
post1: async () => {
/* ... */
},
});
2. Minimal Data
// β
Correct: Only needed fields
export const loadFixtures = createFixtureLoader({
basicUser: async () => {
const userModel = new UserModel();
const { user } = await userModel.create({
username: "user",
email: "user@example.com",
password: "password",
});
return user;
},
});
// β Wrong: Unnecessary fields
export const loadFixtures = createFixtureLoader({
complexUser: async () => {
const userModel = new UserModel();
const { user } = await userModel.create({
username: "user",
email: "user@example.com",
password: "password",
bio: "Long bio...",
avatar: "https://...",
preferences: {
/* ... */
},
// ... too many fields
});
return user;
},
});
3. Fixture Reuse
// β
Correct: Reuse in other fixtures
export const loadFixtures = createFixtureLoader({
user: async () => {
const userModel = new UserModel();
const { user } = await userModel.create({
username: "user",
email: "user@example.com",
password: "password",
});
return user;
},
userPost: async () => {
const { user } = await loadFixtures(["user"]);
const postModel = new PostModel();
const { post } = await postModel.create({
title: "Post",
content: "Content",
author_id: user.id,
});
return post;
},
});
Cautions
Cautions when using Fixtures: 1. Transaction based: Auto rollback for each test 2.
Minimal data: Create only minimum data needed for tests 3. Independence: Each test should
run independently 4. Type safety: Ensure type safety with createFixtureLoader 5. Clear
names: Fixture names should clearly express purpose
Next Steps
Test Structure
bootstrap, test, testAs
Test Scaffolding
Auto test generation
Model System
Understanding Models
Vitest Docs
Vitest guide