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
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
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
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
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
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
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
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:
// 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
# 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