Skip to main content
Sonamu automatically generates model test files with the 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

pnpm generate
When an Entity is defined, {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