AgentSkillsCN

Testing

测试

SKILL.md

Testing Skill

This skill guides you through writing tests for this Bun + Hono project.

Test Configuration

  • Test Runner: Bun's built-in test runner
  • Test Database: SQLite (.env.test)
  • HTTP Client: Axios
  • Isolation: Transaction rollback per test

Directory Structure

code
test/
├── api/           # API endpoint tests
│   └── users.test.ts
├── web/           # Web route tests
│   └── home.test.ts
├── utils/         # Test utilities
│   ├── setup.ts       # Test server & DB setup
│   ├── http-client.ts # Axios instance
│   ├── factories.ts   # Test data factories
│   └── assertions.ts  # Custom assertions
└── README.md

Writing a Basic Test

typescript
import { describe, expect, test } from "bun:test"
import { http } from "../utils/http-client"
import "../utils/setup"  // Required: enables DB transaction rollback

describe("User API", () => {
  describe("GET /api/users", () => {
    test("should return user list", async () => {
      const response = await http.get("/api/users")

      expect(response.status).toBe(200)
      expect(Array.isArray(response.data)).toBe(true)
    })
  })

  describe("POST /api/users", () => {
    test("should create user with valid data", async () => {
      const userData = {
        name: "Test User",
        email: "test@example.com",
        password: "password123",
        password_confirmation: "password123"
      }

      const response = await http.post("/api/users", userData)

      expect(response.status).toBe(200)
      expect(response.data).toHaveProperty("id")
      expect(response.data.name).toBe(userData.name)
    })

    test("should return 422 for invalid email", async () => {
      const response = await http.post("/api/users", {
        name: "Test",
        email: "invalid-email",
        password: "password123",
        password_confirmation: "password123"
      })

      expect(response.status).toBe(422)
      expect(response.data).toHaveProperty("errors")
    })
  })
})

HTTP Client Usage

typescript
import { http } from "../utils/http-client"

// GET request
const response = await http.get("/api/users")
const response = await http.get("/api/users?status=active")
const response = await http.get("/api/users/1")

// POST request (JSON)
const response = await http.post("/api/users", {
  name: "Test User",
  email: "test@example.com"
})

// POST request (FormData)
const formData = new FormData()
formData.append("name", "Test User")
formData.append("avatar", file)
const response = await http.post("/api/users", formData)

// PUT request
const response = await http.put("/api/users/1", { name: "Updated" })

// DELETE request
const response = await http.delete("/api/users/1")

// Response structure
response.status  // HTTP status code
response.data    // Response body (parsed JSON)

Test Factories

typescript
import {
  createUserData,
  createImageFile,
  createFormData
} from "../utils/factories"

// Create user data with defaults
const userData = createUserData()
// {
//   name: "Test User",
//   email: "test-{uuid}@example.com",
//   password: "password123",
//   password_confirmation: "password123"
// }

// Override specific fields
const userData = createUserData({
  name: "Custom Name",
  email: "custom@example.com"
})

// Create test image file
const avatar = createImageFile()
const avatar = createImageFile("custom-name.png")

// Create FormData from object
const formData = createFormData({
  name: "Test User",
  email: "test@example.com",
  avatar: createImageFile()
})

Custom Assertions

typescript
import {
  expectSuccess,
  expectValidationError,
  expectProperties
} from "../utils/assertions"

// Check successful response (2xx)
expectSuccess(response)

// Check validation error (422)
expectValidationError(response)
expectValidationError(response, "email")  // Check specific field

// Check object has properties
expectProperties(response.data, ["id", "name", "email"])

Complete Test Example

typescript
import { describe, expect, test, beforeEach } from "bun:test"
import { http } from "../utils/http-client"
import {
  createUserData,
  createImageFile,
  createFormData
} from "../utils/factories"
import {
  expectSuccess,
  expectValidationError,
  expectProperties
} from "../utils/assertions"
import "../utils/setup"

describe("User API", () => {
  describe("POST /api/users", () => {
    test("should create user with all fields", async () => {
      const userData = createUserData()
      const avatar = createImageFile()
      const formData = createFormData({ ...userData, avatar })

      const response = await http.post("/api/users", formData)

      expectSuccess(response)
      expectProperties(response.data, ["id", "name", "email", "avatar"])
      expect(response.data.name).toBe(userData.name)
      expect(response.data.email).toBe(userData.email)
    })

    test("should reject missing required fields", async () => {
      const response = await http.post("/api/users", {})

      expectValidationError(response)
    })

    test("should reject invalid email format", async () => {
      const userData = createUserData({ email: "not-an-email" })
      const formData = createFormData(userData)

      const response = await http.post("/api/users", formData)

      expectValidationError(response, "email")
    })

    test("should reject password mismatch", async () => {
      const userData = createUserData({
        password: "password123",
        password_confirmation: "different"
      })
      const formData = createFormData(userData)

      const response = await http.post("/api/users", formData)

      expectValidationError(response)
    })

    test("should reject short password", async () => {
      const userData = createUserData({
        password: "short",
        password_confirmation: "short"
      })
      const formData = createFormData(userData)

      const response = await http.post("/api/users", formData)

      expectValidationError(response)
    })
  })

  describe("GET /api/users", () => {
    test("should return empty array initially", async () => {
      const response = await http.get("/api/users")

      expectSuccess(response)
      expect(response.data).toEqual([])
    })

    test("should return created users", async () => {
      // Create a user first
      const userData = createUserData()
      const avatar = createImageFile()
      const formData = createFormData({ ...userData, avatar })
      await http.post("/api/users", formData)

      // Get users
      const response = await http.get("/api/users")

      expectSuccess(response)
      expect(response.data.length).toBe(1)
      expect(response.data[0].name).toBe(userData.name)
    })
  })
})

Testing File Uploads

typescript
import { createImageFile, createFormData } from "../utils/factories"

test("should upload avatar", async () => {
  const userData = createUserData()
  const avatar = createImageFile("avatar.png")
  const formData = createFormData({ ...userData, avatar })

  const response = await http.post("/api/users", formData)

  expectSuccess(response)
  expect(response.data.avatar).toContain("avatars/")
})

test("should reject invalid file type", async () => {
  const userData = createUserData()
  // Create non-image file
  const file = new File(["content"], "file.txt", { type: "text/plain" })
  const formData = createFormData({ ...userData, avatar: file })

  const response = await http.post("/api/users", formData)

  expectValidationError(response)
})

Database Isolation

Tests are automatically isolated using database transactions:

typescript
// test/utils/setup.ts
beforeEach(async () => {
  await lucid.db.beginGlobalTransaction()
})

afterEach(async () => {
  await lucid.db.rollbackGlobalTransaction()
})

Each test runs in its own transaction that gets rolled back, ensuring:

  • Tests don't affect each other
  • Database is clean for each test
  • No cleanup code needed

Running Tests

bash
# Run all tests
bun run test

# Run specific test file
bun test test/api/users.test.ts

# Run tests in watch mode
bun run test:watch

# Run with coverage
bun test --coverage

Test Checklist

  • Test successful operations (happy path)
  • Test validation errors for each field
  • Test missing required fields
  • Test invalid data formats
  • Test edge cases (empty strings, null values)
  • Test file upload validation (if applicable)
  • Test authentication/authorization (if applicable)
  • Test relationships and data integrity