| name | typescript-testing |
| description | TypeScript/JavaScript testing practices with Bun's test runner. Activate when working with bun test, .test.ts, .test.js, .spec.ts, .spec.js, testing TypeScript/JavaScript, bunfig.toml, testing configuration, or test-related tasks in Bun projects. |
TypeScript Testing with Bun
TypeScript/JavaScript-specific testing patterns and best practices using Bun's built-in test runner, complementing general testing-workflow skill.
CRITICAL: Bun Test Execution
NEVER use jest, vitest, or other test runners in Bun projects:
# ✅ CORRECT - Bun test execution
bun test
bun test --watch
bun test src/__tests__
bun test --coverage
bun test --bail
bun test tests/unit.test.ts
# ❌ WRONG - Never use jest in Bun projects
# ❌ jest
# ❌ jest --watch
# ❌ npm run test (if mapped to jest)
# ❌ WRONG - Never use vitest in Bun projects
# ❌ vitest
# ❌ vitest run
Always use bun test directly (never use jest/vitest in Bun projects).
Test File Organization
File Naming Conventions
Bun recognizes test files by standard conventions:
src/
├── utils/
│ ├── math.ts
│ ├── math.test.ts # ✅ Standard .test.ts
│ ├── string-utils.spec.ts # ✅ Alternative .spec.ts
│ └── validation/
│ ├── validator.ts
│ └── validator.test.ts
├── services/
│ ├── api.ts
│ └── __tests__/ # ✅ __tests__ directory
│ └── api.test.ts
└── components/
├── Button.tsx
└── Button.test.tsx # ✅ React component tests
Discovery Patterns
Bun automatically finds tests matching:
*.test.ts/*.test.tsx*.test.js/*.test.jsx*.spec.ts/*.spec.tsx*.spec.js/*.spec.jsx- Files in
__tests__directories
Basic Test Structure
Simple Test Example
import { describe, it, expect } from "bun:test";
import { add, multiply } from "./math";
describe("Math utilities", () => {
it("should add two numbers", () => {
expect(add(2, 3)).toBe(5);
});
it("should multiply two numbers", () => {
expect(multiply(4, 5)).toBe(20);
});
it("should handle negative numbers", () => {
expect(add(-1, -2)).toBe(-3);
});
});
Describe Blocks
Organize tests with nested describe blocks:
import { describe, it, expect } from "bun:test";
import { UserService } from "./user-service";
describe("UserService", () => {
describe("create", () => {
it("should create user with valid data", () => {
// Test implementation
});
it("should throw error on invalid email", () => {
// Test implementation
});
});
describe("update", () => {
it("should update user properties", () => {
// Test implementation
});
it("should not update protected fields", () => {
// Test implementation
});
});
describe("delete", () => {
it("should delete user by id", () => {
// Test implementation
});
});
});
Bun Test API
Describe and It
import { describe, it, expect } from "bun:test";
describe("Feature name", () => {
it("should do something", () => {
expect(true).toBe(true);
});
it("should handle edge case", () => {
expect(() => riskyOperation()).toThrow();
});
});
Common Assertions
import { expect } from "bun:test";
// Equality
expect(value).toBe(5); // Strict equality (===)
expect(obj).toEqual({ a: 1 }); // Deep equality
expect(value).toStrictEqual(5); // Strict deep equality
// Truthiness
expect(value).toBeTruthy(); // Truthy value
expect(value).toBeFalsy(); // Falsy value
expect(value).toBeNull(); // null
expect(value).toBeUndefined(); // undefined
expect(value).toBeDefined(); // Not undefined
// Numbers
expect(number).toBeGreaterThan(5);
expect(number).toBeGreaterThanOrEqual(5);
expect(number).toBeLessThan(10);
expect(number).toBeLessThanOrEqual(10);
expect(0.1 + 0.2).toBeCloseTo(0.3); // Float comparison
// Strings
expect(string).toMatch(/pattern/);
expect(string).toContain("substring");
// Arrays
expect(array).toContain(value);
expect(array).toHaveLength(3);
// Objects
expect(obj).toHaveProperty("key");
expect(obj).toHaveProperty("key", expectedValue);
// Exceptions
expect(() => throwError()).toThrow();
expect(() => throwError()).toThrow(CustomError);
expect(() => throwError()).toThrow(/error message/);
Skipping and Only
import { it, describe } from "bun:test";
describe("Feature", () => {
it("should test this", () => {
// Runs
});
it.skip("should skip this test", () => {
// Skipped
});
it.only("should run only this test", () => {
// Only this runs in the suite
});
describe.skip("skipped suite", () => {
it("won't run", () => {});
});
});
Test.todo
import { it } from "bun:test";
it.todo("feature not yet implemented");
it.todo("edge case to handle");
Setup and Teardown
beforeEach and afterEach
import { describe, it, beforeEach, afterEach, expect } from "bun:test";
import { Database } from "./database";
describe("Database operations", () => {
let db: Database;
beforeEach(() => {
// Setup before each test
db = new Database(":memory:");
db.initialize();
});
afterEach(() => {
// Cleanup after each test
db.close();
});
it("should insert and retrieve data", () => {
db.insert("users", { id: 1, name: "John" });
const user = db.query("SELECT * FROM users WHERE id = 1");
expect(user.name).toBe("John");
});
});
beforeAll and afterAll
import { describe, it, beforeAll, afterAll, expect } from "bun:test";
import { setupExpensiveResource } from "./resources";
describe("Resource-intensive operations", () => {
let resource: any;
beforeAll(() => {
// Setup once for entire suite
resource = setupExpensiveResource();
});
afterAll(() => {
// Cleanup once after entire suite
resource.teardown();
});
it("uses expensive resource", () => {
expect(resource.isReady()).toBe(true);
});
it("performs operation", () => {
const result = resource.process("data");
expect(result).toBeDefined();
});
});
Nested Hooks
import { describe, it, beforeEach } from "bun:test";
describe("Outer suite", () => {
let value = 0;
beforeEach(() => {
value = 10;
});
it("test in outer", () => {
expect(value).toBe(10);
});
describe("Inner suite", () => {
beforeEach(() => {
value *= 2; // Runs after outer beforeEach
});
it("test in inner", () => {
expect(value).toBe(20); // 10 * 2
});
});
});
Mocking with Bun
Using mock()
import { mock } from "bun:test";
import { fetchUser } from "./api";
const mockFetch = mock((userId: string) => {
return { id: userId, name: "Mock User" };
});
// Test mock behavior
const result = mockFetch("123");
expect(result.name).toBe("Mock User");
expect(mockFetch.mock.calls.length).toBe(1);
expect(mockFetch.mock.calls[0]).toEqual(["123"]);
Mock Objects and Modules
import { describe, it, expect, mock } from "bun:test";
describe("Service with mocked dependency", () => {
it("should use mocked database", () => {
const mockDb = {
query: mock((sql: string) => [{ id: 1, name: "Test" }]),
close: mock(() => {}),
};
const service = new Service(mockDb);
const result = service.getUser(1);
expect(result.name).toBe("Test");
expect(mockDb.query.mock.calls.length).toBe(1);
});
});
Module Mocking
import { describe, it, expect, mock } from "bun:test";
import { getUserFromAPI } from "./api";
// Mock entire modules
mock.module("./api", () => ({
getUserFromAPI: mock((id: string) => ({
id,
name: "Mocked User",
})),
}));
describe("API integration", () => {
it("should work with mocked API", async () => {
const user = await getUserFromAPI("123");
expect(user.name).toBe("Mocked User");
});
});
Spy on Function Calls
import { describe, it, expect, mock } from "bun:test";
describe("Spy on calls", () => {
it("should track function calls", () => {
const originalFunc = (x: number) => x * 2;
const spied = mock(originalFunc);
const result1 = spied(5);
const result2 = spied(10);
expect(result1).toBe(10);
expect(result2).toBe(20);
expect(spied.mock.calls.length).toBe(2);
expect(spied.mock.results[0].value).toBe(10);
expect(spied.mock.results[1].value).toBe(20);
});
});
Mock Return Values
import { describe, it, expect, mock } from "bun:test";
describe("Mock return values", () => {
it("should return configured values", () => {
const mockFunc = mock();
// Set return values for specific calls
mockFunc.mock.returns = [
{ value: "first" },
{ value: "second" },
{ value: "third" },
];
expect(mockFunc()).toEqual({ value: "first" });
expect(mockFunc()).toEqual({ value: "second" });
});
it("should throw errors when configured", () => {
const errorMock = mock(() => {
throw new Error("Mocked error");
});
expect(() => errorMock()).toThrow("Mocked error");
});
});
Async Testing
Async/Await in Tests
import { describe, it, expect } from "bun:test";
import { fetchUser } from "./api";
describe("Async operations", () => {
it("should fetch user data", async () => {
const user = await fetchUser("123");
expect(user.id).toBe("123");
expect(user.name).toBeDefined();
});
it("should handle fetch errors", async () => {
expect(fetchUser("invalid")).rejects.toThrow();
});
});
Promise Testing
import { describe, it, expect } from "bun:test";
describe("Promise handling", () => {
it("should resolve with data", () => {
const promise = Promise.resolve({ id: 1, name: "User" });
return expect(promise).resolves.toEqual({ id: 1, name: "User" });
});
it("should reject with error", () => {
const promise = Promise.reject(new Error("Failed"));
return expect(promise).rejects.toThrow("Failed");
});
});
Concurrent Async Tests
import { describe, it, expect } from "bun:test";
describe("Concurrent operations", () => {
it("should handle multiple concurrent requests", async () => {
const results = await Promise.all([
fetchData("1"),
fetchData("2"),
fetchData("3"),
]);
expect(results).toHaveLength(3);
expect(results[0].id).toBe("1");
expect(results[1].id).toBe("2");
expect(results[2].id).toBe("3");
});
it("should race multiple promises", async () => {
const winner = await Promise.race([
slowOperation(100),
slowOperation(50),
slowOperation(200),
]);
expect(winner).toBeDefined();
});
});
Test Fixtures and Utilities
Shared Test Data
// tests/fixtures/users.ts
export const testUsers = {
admin: {
id: "1",
email: "admin@example.com",
role: "admin",
},
user: {
id: "2",
email: "user@example.com",
role: "user",
},
guest: {
id: "3",
email: "guest@example.com",
role: "guest",
},
};
export const invalidUsers = {
noEmail: { id: "4" },
invalidEmail: { id: "5", email: "not-an-email" },
noId: { email: "test@example.com" },
};
// In test file
import { describe, it, expect } from "bun:test";
import { testUsers } from "./fixtures/users";
describe("User roles", () => {
it("should verify admin role", () => {
expect(testUsers.admin.role).toBe("admin");
});
});
Fixture Setup Function
// tests/fixtures/setup.ts
export function createMockUser(overrides: Partial<User> = {}): User {
return {
id: "test-id",
email: "test@example.com",
name: "Test User",
role: "user",
createdAt: new Date(),
...overrides,
};
}
export function createMockDatabase() {
const users: User[] = [];
return {
addUser: (user: User) => {
users.push(user);
return user;
},
getUser: (id: string) => users.find(u => u.id === id),
getAllUsers: () => [...users],
clear: () => users.splice(0),
};
}
// In test
import { describe, it, beforeEach, expect } from "bun:test";
import { createMockUser, createMockDatabase } from "./fixtures/setup";
describe("User repository", () => {
let db: ReturnType<typeof createMockDatabase>;
beforeEach(() => {
db = createMockDatabase();
});
it("should add and retrieve users", () => {
const user = createMockUser({ name: "John Doe" });
db.addUser(user);
expect(db.getUser(user.id)?.name).toBe("John Doe");
});
});
Coverage with Bun
Running Coverage
# Generate coverage report
bun test --coverage
# Coverage with specific files
bun test --coverage src/
# HTML coverage report
bun test --coverage --coverage-html
Configuration in bunfig.toml
[test]
# Enable coverage
coverage = true
# Coverage reporting format
coverageFormat = ["text", "html", "json"]
# Files to report on
coverageThreshold = 80
# Exclude from coverage
coverageIgnore = ["**/node_modules/**", "**/dist/**"]
# Root directory for coverage
coverageRoot = "src"
Coverage Reports
# Text report
bun test --coverage
# Generate HTML report in coverage/
bun test --coverage --coverage-html
# JSON report for CI/CD
bun test --coverage coverage/coverage.json
Integration Testing
Testing HTTP APIs
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
import { startServer, stopServer } from "./server";
describe("API Integration", () => {
let baseUrl: string;
beforeAll(async () => {
const server = await startServer();
baseUrl = `http://localhost:${server.port}`;
});
afterAll(async () => {
await stopServer();
});
it("should create a user", async () => {
const response = await fetch(`${baseUrl}/api/users`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "test@example.com" }),
});
expect(response.status).toBe(201);
const data = await response.json();
expect(data.id).toBeDefined();
});
it("should retrieve user", async () => {
const response = await fetch(`${baseUrl}/api/users/1`);
expect(response.status).toBe(200);
});
});
Database Integration
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
import { Database } from "./database";
describe("Database operations", () => {
let db: Database;
beforeAll(async () => {
db = new Database(":memory:");
await db.initialize();
await db.runMigrations();
});
afterAll(async () => {
await db.close();
});
it("should perform CRUD operations", async () => {
// Create
const user = await db.users.create({
email: "test@example.com",
name: "Test User",
});
expect(user.id).toBeDefined();
// Read
const retrieved = await db.users.findById(user.id);
expect(retrieved.email).toBe("test@example.com");
// Update
await db.users.update(user.id, { name: "Updated" });
const updated = await db.users.findById(user.id);
expect(updated.name).toBe("Updated");
// Delete
await db.users.delete(user.id);
const deleted = await db.users.findById(user.id);
expect(deleted).toBeNull();
});
});
Testing TypeScript Types
Type Testing with TypeScript
import { describe, it, expectTypeOf } from "bun:test";
import { processUser } from "./user-processor";
describe("Type safety", () => {
it("should have correct return type", () => {
const result = processUser({ name: "John", age: 30 });
// Check type at compile time
expectTypeOf(result).toMatchTypeOf<{ success: boolean }>();
});
it("should enforce parameter types", () => {
// TypeScript will catch these at compile time
// @ts-expect-error - wrong type
processUser({ name: 123 });
// @ts-expect-error - missing required field
processUser({ age: 30 });
});
});
Configuration in bunfig.toml
Complete Test Configuration
[test]
# Test file patterns
root = "."
prefix = ""
suffix = [".test", ".spec"]
testNamePattern = ""
# Coverage
coverage = true
coverageFormat = ["text", "html", "json"]
coverageThreshold = 80
coverageRoot = "src"
coverageIgnore = ["**/node_modules/**"]
# Test execution
bail = false
timeout = 30000
reportFailures = true
# Reporters
reporters = ["spec"] # or ["tap", "junit"]
# Output
preloadModules = []
With npm scripts in package.json
{
"scripts": {
"test": "bun test",
"test:watch": "bun test --watch",
"test:coverage": "bun test --coverage",
"test:ui": "bun test --coverage --coverage-html",
"test:single": "bun test tests/unit.test.ts",
"test:bail": "bun test --bail",
"test:debug": "bun test --inspect-brk"
}
}
React Component Testing
Testing React Components
import { describe, it, expect } from "bun:test";
import { render, screen } from "bun:test:dom";
import { Button } from "./Button";
describe("Button component", () => {
it("should render button with text", () => {
render(<Button label="Click me" />);
const button = screen.getByRole("button", { name: "Click me" });
expect(button).toBeDefined();
});
it("should call onClick handler", async () => {
const handleClick = mock();
render(<Button label="Click" onClick={handleClick} />);
const button = screen.getByRole("button");
button.click();
expect(handleClick.mock.calls.length).toBe(1);
});
it("should disable button when disabled prop is true", () => {
render(<Button label="Disabled" disabled={true} />);
const button = screen.getByRole("button") as HTMLButtonElement;
expect(button.disabled).toBe(true);
});
});
Common Testing Patterns
Arrange-Act-Assert
import { describe, it, expect } from "bun:test";
import { calculateTotal } from "./calculator";
describe("calculateTotal", () => {
it("should sum array of numbers", () => {
// Arrange - Set up test data
const items = [
{ price: 10, quantity: 2 },
{ price: 5, quantity: 3 },
];
// Act - Execute functionality
const total = calculateTotal(items);
// Assert - Verify results
expect(total).toBe(35); // (10*2) + (5*3)
});
});
Testing Error Conditions
import { describe, it, expect } from "bun:test";
import { validateEmail } from "./validators";
describe("validateEmail", () => {
it("should validate correct email", () => {
expect(validateEmail("test@example.com")).toBe(true);
});
it("should reject invalid emails", () => {
expect(validateEmail("not-an-email")).toBe(false);
expect(validateEmail("@example.com")).toBe(false);
expect(validateEmail("test@")).toBe(false);
});
it("should throw on null input", () => {
expect(() => validateEmail(null as any)).toThrow();
});
});
Testing Class Methods
import { describe, it, expect, beforeEach } from "bun:test";
import { Counter } from "./counter";
describe("Counter class", () => {
let counter: Counter;
beforeEach(() => {
counter = new Counter();
});
it("should increment", () => {
counter.increment();
expect(counter.value).toBe(1);
});
it("should decrement", () => {
counter.increment();
counter.decrement();
expect(counter.value).toBe(0);
});
it("should reset to zero", () => {
counter.increment();
counter.increment();
counter.reset();
expect(counter.value).toBe(0);
});
});
Edge Case Testing
Common Edge Cases for TypeScript
import { describe, it, expect } from "bun:test";
import { processArray } from "./processor";
describe("processArray edge cases", () => {
it("should handle empty array", () => {
expect(processArray([])).toEqual([]);
});
it("should handle single item", () => {
expect(processArray([1])).toEqual([1]);
});
it("should handle undefined values", () => {
const result = processArray([1, undefined, 3]);
expect(result).toContain(1);
expect(result).toContain(3);
});
it("should handle null values", () => {
const result = processArray([1, null, 3]);
expect(result.length).toBeLessThanOrEqual(3);
});
it("should handle very large numbers", () => {
const large = Number.MAX_SAFE_INTEGER;
expect(processArray([large, large])).toBeDefined();
});
it("should handle special values", () => {
expect(processArray([0, -0, NaN])).toBeDefined();
});
});
Null/Undefined Handling
import { describe, it, expect } from "bun:test";
import { getUser } from "./user-service";
describe("Null/undefined handling", () => {
it("should return null for missing user", async () => {
const user = await getUser("nonexistent");
expect(user).toBeNull();
});
it("should handle undefined optional fields", async () => {
const user = await getUser("123");
if (user) {
expect(user.middleName).toBeUndefined();
}
});
it("should distinguish null from undefined", () => {
const nullValue = null;
const undefinedValue = undefined;
expect(nullValue).toBeNull();
expect(undefinedValue).toBeUndefined();
expect(nullValue).not.toBe(undefinedValue);
});
});
Zero-Warnings Policy
Treat Warnings as Errors
Running tests should produce zero warnings:
# Run tests and fail on any warnings
bun test
# If warnings appear, identify and fix them
# Common causes:
# - Deprecated API usage
# - Unhandled promise rejections
# - Memory leaks in tests
# - Resource cleanup issues
Configuration for Warnings
[test]
# Fail on warnings (if available in your Bun version)
reportFailures = true
# Configure reporters to show warnings
reporters = ["spec"]
Handling Expected Warnings
If a library produces unavoidable warnings:
import { describe, it, expect } from "bun:test";
describe("Feature with expected warning", () => {
it("should work despite library warning", () => {
// This test runs code that produces a library warning
// Document why the warning is acceptable
expect(unsafeLibraryFunction()).toBeDefined();
});
});
Makefile Integration
Test Targets
.PHONY: test test-watch test-coverage test-single test-bail
# Run all tests
test:
bun test
# Watch mode - rerun on file changes
test-watch:
bun test --watch
# Run with coverage
test-coverage:
bun test --coverage
# View HTML coverage report
test-ui:
bun test --coverage --coverage-html
@echo "Coverage report: coverage/index.html"
# Run single test file
test-single:
bun test tests/specific.test.ts
# Fail on first error
test-bail:
bun test --bail
# Debug tests
test-debug:
bun test --inspect-brk
# Full test suite with checks
check: test lint type-check
@echo "All checks passed!"
Project Structure Patterns
Organized Test Structure
src/
├── utils/
│ ├── math.ts
│ ├── math.test.ts # Colocated with source
│ └── string.ts
├── services/
│ ├── api.ts
│ └── api.test.ts
├── __tests__/ # Alternative: centralized tests
│ ├── fixtures/
│ │ ├── users.ts
│ │ └── setup.ts
│ ├── unit/
│ │ └── math.test.ts
│ ├── integration/
│ │ └── api.test.ts
│ └── e2e/
│ └── workflow.test.ts
└── index.ts
Test Fixtures Directory
tests/
├── fixtures/
│ ├── users.ts # Test user data
│ ├── database.ts # Test database setup
│ ├── api-responses.ts # Mock API responses
│ └── setup.ts # Fixture functions
└── helpers/
├── assertions.ts # Custom assertions
└── mocks.ts # Mock utilities
Dependency Installation
Adding Test Dependencies
# Core testing (already built-in with Bun)
# No installation needed - use "bun:test"
# Additional testing utilities (optional)
bun add --dev @types/bun
# React testing (if using React)
bun add --dev jsdom
# HTTP testing utilities
bun add --dev node-fetch @types/node
# Test data generation
bun add --dev faker
Note: For general testing principles and strategies not specific to TypeScript/JavaScript, see the testing-workflow skill.