typescript-testing

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.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "typescript-testing" with this command: npx skills add ilude/claude-code-config/ilude-claude-code-config-typescript-testing

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.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

claude-code-workflow

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

container-workflow

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

code-documentation

No summary provided by upstream source.

Repository SourceNeeds Review
typescript-testing | V50.AI