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