Test Helpers Overview
Fixture Loader
Test data reuseType-safe loading
Fixture Manager
Copy data between DBsAuto relationship handling
Easy Setup
Define onceShare across tests
Auto Cleanup
Transaction basedAuto rollback
Fixture Loader
createFixtureLoader
Pre-define fixtures for tests and load when needed.Copy
// 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
Copy
// 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:Copy
// ✅ Type safe: IDE provides auto-completion
const { user01 } = await loadFixtures(["user01"]);
user01.username; // string
// ❌ Non-existent fixture
const { user999 } = await loadFixtures(["user999"]); // Type error!
Copy
// Use same fixture in multiple tests
test("Test 1", async () => {
const { user01 } = await loadFixtures(["user01"]);
// ...
});
test("Test 2", async () => {
const { user01 } = await loadFixtures(["user01"]);
// ...
});
Copy
// Load only what's needed
const { user01, user02 } = await loadFixtures(["user01", "user02"]);
// admin not loaded → Save creation cost
Complex Fixture Patterns
Fixtures with Relationships
Copy
// 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;
},
});
Copy
// 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
Copy
// 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.Copy
// 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.Copy
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.)
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// ✅ 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
Copy
// ✅ 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
Copy
// ✅ 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:
- Transaction based: Auto rollback for each test
- Minimal data: Create only minimum data needed for tests
- Independence: Each test should run independently
- Type safety: Ensure type safety with createFixtureLoader
- Clear names: Fixture names should clearly express purpose