pnpm generate command.
Scaffolding Overview
Auto Generation
model.test.ts creationBasic structure included
Bootstrap Included
Auto test environment setupImmediately executable
CRUD Tests
Basic test skeletonCustomizable
Type Safe
Model integrationAuto-completion support
Test File Generation
Command
Copy
pnpm generate
{model}.test.ts files are automatically generated.
Generation location:
Copy
api/src/models/
βββ user.model.ts
βββ user.model.test.ts # Auto-generated
βββ post.model.ts
βββ post.model.test.ts # Auto-generated
Generated File Structure
Copy
// api/src/models/user.model.test.ts
import { bootstrap, test } from "sonamu/test";
import { expect, vi } from "vitest";
import { UserModel } from "./user.model";
// Initialize test environment
bootstrap(vi);
test("Create user", async () => {
const userModel = new UserModel();
// Write test code
const { user } = await userModel.create({
username: "john",
email: "john@example.com",
password: "password123",
});
expect(user.id).toBeGreaterThan(0);
expect(user.username).toBe("john");
});
test("Get user", async () => {
const userModel = new UserModel();
// Create test data
const { user } = await userModel.create({
username: "jane",
email: "jane@example.com",
password: "password123",
});
// Test retrieval
const { user: found } = await userModel.getUser("C", user.id);
expect(found.id).toBe(user.id);
expect(found.username).toBe("jane");
});
Running Tests
Run Single File
Copy
# Run only specific model test
pnpm vitest user.model.test.ts
Run All Tests
Copy
# Run all tests
pnpm test
# Or
pnpm vitest
Watch Mode
Copy
# Auto re-run on file changes
pnpm vitest --watch
Test Writing Guide
CRUD Test Pattern
Copy
import { bootstrap, test } from "sonamu/test";
import { expect, vi } from "vitest";
import { PostModel } from "./post.model";
bootstrap(vi);
// Create
test("Create post", async () => {
const postModel = new PostModel();
const { post } = await postModel.create({
title: "Test Post",
content: "Test Content",
author_id: 1,
});
expect(post.id).toBeGreaterThan(0);
expect(post.title).toBe("Test Post");
});
// Read
test("Get post", async () => {
const postModel = new PostModel();
// Create test data
const { post } = await postModel.create({
title: "Test Post",
content: "Test Content",
author_id: 1,
});
// Retrieve
const { post: found } = await postModel.getPost("C", post.id);
expect(found.id).toBe(post.id);
expect(found.title).toBe("Test Post");
});
// Update
test("Update post", async () => {
const postModel = new PostModel();
// Create test data
const { post } = await postModel.create({
title: "Original Title",
content: "Original Content",
author_id: 1,
});
// Update
await postModel.update(post.id, {
title: "Updated Title",
});
// Verify
const { post: updated } = await postModel.getPost("C", post.id);
expect(updated.title).toBe("Updated Title");
expect(updated.content).toBe("Original Content"); // Not changed
});
// Delete
test("Delete post", async () => {
const postModel = new PostModel();
// Create test data
const { post } = await postModel.create({
title: "Test Post",
content: "Test Content",
author_id: 1,
});
// Delete
await postModel.delete(post.id);
// Verify
const deleted = await postModel.findById("C", post.id);
expect(deleted).toBeNull();
});
Relationship Tests
Copy
test("User and post relationship", async () => {
const userModel = new UserModel();
const postModel = new PostModel();
// Create user
const { user } = await userModel.create({
username: "author",
email: "author@example.com",
password: "password",
});
// Create post
const { post } = await postModel.create({
title: "User's Post",
content: "Content",
author_id: user.id,
});
// Verify relationship
expect(post.author_id).toBe(user.id);
// Querying with Subset "C" includes author info
const { post: fullPost } = await postModel.getPost("C", post.id);
expect(fullPost.author?.username).toBe("author");
});
Error Tests
Copy
test("Duplicate email validation", async () => {
const userModel = new UserModel();
// Create first user
await userModel.create({
username: "user1",
email: "duplicate@example.com",
password: "password",
});
// Try to create with same email
await expect(
userModel.create({
username: "user2",
email: "duplicate@example.com",
password: "password",
})
).rejects.toThrow("Email already exists");
});
test("Get non-existent post", async () => {
const postModel = new PostModel();
await expect(
postModel.getPost("C", 999999)
).rejects.toThrow("Post not found");
});
Structuring Tests
Grouping Multiple Tests
Copy
import { bootstrap, test } from "sonamu/test";
import { expect, vi } from "vitest";
import { describe } from "vitest";
import { UserModel } from "./user.model";
bootstrap(vi);
describe("User creation", () => {
test("Normal creation", async () => {
const userModel = new UserModel();
const { user } = await userModel.create({
username: "john",
email: "john@example.com",
password: "password",
});
expect(user.id).toBeGreaterThan(0);
});
test("Duplicate email validation", async () => {
const userModel = new UserModel();
await userModel.create({
username: "user1",
email: "duplicate@example.com",
password: "password",
});
await expect(
userModel.create({
username: "user2",
email: "duplicate@example.com",
password: "password",
})
).rejects.toThrow();
});
});
describe("User retrieval", () => {
test("Get by ID", async () => {
const userModel = new UserModel();
const { user } = await userModel.create({
username: "test",
email: "test@example.com",
password: "password",
});
const { user: found } = await userModel.getUser("C", user.id);
expect(found.id).toBe(user.id);
});
test("Get non-existent user", async () => {
const userModel = new UserModel();
await expect(
userModel.getUser("C", 999999)
).rejects.toThrow();
});
});
Best Practices
1. Test Isolation
Each test should run independently.Copy
// β
Correct: Each test is independent
test("Test 1", async () => {
const userModel = new UserModel();
const { user } = await userModel.create({ /* ... */ });
// Auto rollback after test ends
});
test("Test 2", async () => {
const userModel = new UserModel();
// Starts with clean DB state
const { user } = await userModel.create({ /* ... */ });
});
// β Wrong: Dependencies between tests
let sharedUserId: number;
test("Test 1", async () => {
const userModel = new UserModel();
const { user } = await userModel.create({ /* ... */ });
sharedUserId = user.id; // β Shared with other tests
});
test("Test 2", async () => {
const userModel = new UserModel();
const { user } = await userModel.getUser("C", sharedUserId); // β Fails!
});
2. Clear Test Names
Copy
// β
Correct
test("Error thrown when creating user with duplicate email", async () => {
// ...
});
// β Wrong
test("Test 1", async () => {
// ...
});
3. Arrange-Act-Assert Pattern
Copy
test("Update post", async () => {
// Arrange
const postModel = new PostModel();
const { post } = await postModel.create({
title: "Original",
content: "Content",
author_id: 1,
});
// Act
await postModel.update(post.id, {
title: "Updated",
});
// Assert
const { post: updated } = await postModel.getPost("C", post.id);
expect(updated.title).toBe("Updated");
});
Cautions
Cautions when writing tests:
- Donβt modify auto-generated files: Will be overwritten on regeneration
- Independent tests: No dependencies between tests
- Transaction based: Auto rollback after each test
- Async required: All tests must be async functions
- Clear names: Clearly state what the test verifies