pnpm generate command.
Scaffolding Overview
Auto Generation
model.test.ts creation Basic structure included
Bootstrap Included
Auto test environment setup Immediately executable
CRUD Tests
Basic test skeleton Customizable
Type Safe
Model integration Auto-completion support
Test File Generation
Command
pnpm generate
{model}.test.ts files are automatically generated.
Generation location:
api/src/models/
βββ user.model.ts
βββ user.model.test.ts # Auto-generated
βββ post.model.ts
βββ post.model.test.ts # Auto-generated
Generated File Structure
// 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
# Run only specific model test
pnpm vitest user.model.test.ts
Run All Tests
# Run all tests
pnpm test
# Or
pnpm vitest
Watch Mode
# Auto re-run on file changes
pnpm vitest --watch
Test Writing Guide
CRUD Test Pattern
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
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
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
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.// β
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
// β
Correct
test("Error thrown when creating user with duplicate email", async () => {
// ...
});
// β Wrong
test("Test 1", async () => {
// ...
});
3. Arrange-Act-Assert Pattern
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: 1. Donβt modify auto-generated files: Will be overwritten on
regeneration 2. Independent tests: No dependencies between tests 3. Transaction based:
Auto rollback after each test 4. Async required: All tests must be async functions 5. Clear
names: Clearly state what the test verifies
Next Steps
Test Structure
bootstrap, test, testAs
Test Helpers
Fixtures and utilities
Vitest Docs
Vitest guide
Model System
Understanding Models