
Bun Test
- 7 installs
- 11 repo stars
- Updated March 8, 2026
- ainergiz/xfeed
bun-test is a Claude Code skill that teaches writing and debugging tests with Bun's bun:test runner, including mocking and coverage.
About
bun-test is a Claude Code skill that teaches writing and debugging tests with Bun's built-in test runner. It covers mock functions, spies, fetch and module mocking, per-test temp-directory isolation, and coverage reporting. Developers use it when writing tests, fixing test failures, or setting up test infrastructure in a Bun project.
- Bun test patterns: mocking, spies, module mocks, and coverage
- fetch and module mocking with restore-after-test isolation
- Temp-directory and state isolation to avoid test races
Bun Test by the numbers
- 7 all-time installs (skills.sh)
- Ranked #1,579 of 2,154 Testing & QA skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
bun-test capabilities & compatibility
- Capabilities
- unit testing · test mocking · coverage reporting
- Use cases
- testing · debugging
What bun-test says it does
Write and debug Bun tests with proper mocking, coverage, and isolation.
Use `mkdtemp()` per test, not a shared temp directory:
npx skills add https://github.com/ainergiz/xfeed --skill bun-testAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 7 |
|---|---|
| repo stars | ★ 11 |
| Last updated | March 8, 2026 |
| Repository | ainergiz/xfeed ↗ |
What it does
Write and debug Bun tests with mocking, isolation, and coverage.
Who is it for?
Developers writing or debugging tests in a Bun project.
Skip if: Projects that use Jest, Vitest, or a non-Bun runtime.
When should I use this skill?
When writing tests, debugging test failures, setting up test infrastructure, or mocking fetch/modules.
What you get
Isolated, correctly mocked Bun tests with meaningful coverage.
- Bun test files
- coverage reports
By the numbers
- aim for 99%+ coverage
- mkdtemp per test for isolation
Files
Bun Test Guide
Quick Start
import { describe, expect, it, mock, spyOn, beforeEach, afterEach, afterAll } from "bun:test";
describe("MyModule", () => {
it("does something", () => {
expect(1 + 1).toBe(2);
});
});Run tests:
bun test # Run all tests
bun test --watch # Watch mode
bun test --coverage # With coverage report
bun test src/api # Specific directory
bun test --test-name-pattern "pattern" # Filter by nameMocking Patterns
Mock Functions
const mockFn = mock(() => "mocked value");
mockFn();
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(1);
// Reset between tests
mockFn.mockReset();
mockFn.mockImplementation(() => "new value");Spy on Object Methods
import { myModule } from "./my-module";
let methodSpy: Mock<typeof myModule.method>;
beforeAll(() => {
methodSpy = spyOn(myModule, "method").mockImplementation(() => "mocked");
});
afterAll(() => {
methodSpy.mockRestore(); // IMPORTANT: Always restore spies
});Mock fetch (globalThis.fetch)
Bun's fetch has extra properties (like preconnect) that mocks don't have. Use // @ts-nocheck at file top for test files with fetch mocking:
// @ts-nocheck - Test file with fetch mocking
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
const originalFetch = globalThis.fetch;
// Helper for mock responses
function mockResponse(body: unknown, options: { status?: number; ok?: boolean } = {}) {
const status = options.status ?? 200;
const ok = options.ok ?? (status >= 200 && status < 300);
return {
ok,
status,
text: () => Promise.resolve(typeof body === "string" ? body : JSON.stringify(body)),
json: () => Promise.resolve(body),
} as Response;
}
describe("API", () => {
afterEach(() => {
globalThis.fetch = originalFetch; // Always restore
});
it("fetches data", async () => {
globalThis.fetch = mock(() => Promise.resolve(mockResponse({ data: "test" })));
const result = await myApi.getData();
expect(result).toEqual({ data: "test" });
});
it("handles errors", async () => {
globalThis.fetch = mock(() => Promise.reject(new Error("Network error")));
await expect(myApi.getData()).rejects.toThrow("Network error");
});
});Mock Modules (External Dependencies)
Use mock.module() BEFORE importing the module under test:
// @ts-nocheck - Test file with module mocking
import { describe, expect, it, mock } from "bun:test";
// Create mutable mock implementation
let mockImpl = () => Promise.resolve({ data: "default" });
// Mock the module BEFORE importing
mock.module("external-package", () => ({
someFunction: () => mockImpl(),
}));
// NOW import the module that uses external-package
const { myFunction } = await import("./my-module");
// Helper to change mock behavior per test
function setMockReturn(value: unknown) {
mockImpl = () => Promise.resolve(value);
}
describe("MyModule", () => {
it("uses external package", async () => {
setMockReturn({ data: "test" });
const result = await myFunction();
expect(result.data).toBe("test");
});
});Test Isolation
State Sharing Warning
Tests within a file share module-level state. Use setup/teardown hooks carefully:
// Store original values at module level
const originalEnv = process.env.NODE_ENV;
const originalFetch = globalThis.fetch;
afterAll(() => {
// Restore everything
globalThis.fetch = originalFetch;
if (originalEnv !== undefined) {
process.env.NODE_ENV = originalEnv;
} else {
delete process.env.NODE_ENV;
}
});Temp Directory Isolation
Use mkdtemp() per test, not a shared temp directory:
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
let testDir: string;
beforeEach(async () => {
// Unique temp dir per test - avoids race conditions
testDir = await mkdtemp(path.join(tmpdir(), "my-test-"));
});
afterEach(async () => {
if (testDir) {
await rm(testDir, { recursive: true, force: true }).catch(() => {});
}
});Coverage
bun test --coverage # Generate text report
bun test --coverage-reporter lcov # For CI/tooling integrationCoverage Quirks
1. Closing braces after return may show uncovered even when executed 2. Function declarations may not count if only body runs 3. 100% may be impossible - aim for 99%+ on meaningful code
Improving Coverage
- Test all branches (if/else, switch cases)
- Test error paths and edge cases
- Test with different input types
- Don't obsess over unreachable code (closing braces, etc.)
Common Patterns
Async Tests
it("handles async", async () => {
const result = await asyncFunction();
expect(result).toBe("expected");
});
it("expects rejection", async () => {
await expect(asyncFunction()).rejects.toThrow("error message");
});Parameterized Tests
const testCases = [
{ input: 1, expected: 2 },
{ input: 2, expected: 4 },
];
for (const { input, expected } of testCases) {
it(`doubles ${input} to ${expected}`, () => {
expect(double(input)).toBe(expected);
});
}Testing Timeouts
it("handles timeout", async () => {
// Use small delays for tests
const result = await functionWithDelay(1); // 1ms instead of 1000ms
expect(result).toBeDefined();
});Checklist for New Test Files
1. Add // @ts-nocheck if mocking fetch or complex types 2. Store original values (fetch, env vars) before modifying 3. Restore everything in afterEach or afterAll 4. Use mkdtemp() for temp directories (not shared paths) 5. Call mockRestore() on spies in afterAll 6. Use descriptive test names that explain the scenario
Debugging Tests
bun test --bail # Stop on first failure
bun test --timeout 30000 # Increase timeout (ms)
bun test --test-name-pattern "specific test" # Run one testAdd console.log for debugging (remove before committing):
it("debugging", () => {
console.log("Value:", someValue);
expect(someValue).toBeDefined();
});Additional Resources
For xfeed-specific patterns (XClient, RuntimeQueryIdStore, cookie mocking, GraphQL responses), see PATTERNS.md.
xfeed Test Patterns
This file contains patterns specific to the xfeed codebase.
XClient Test Pattern
// @ts-nocheck - Test file with fetch mocking
import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
it,
mock,
spyOn,
type Mock,
} from "bun:test";
import { XClient } from "./client";
import { runtimeQueryIds } from "./query-ids";
// Mock implementations
const mockGetQueryId = mock(() => Promise.resolve(null));
const mockRefresh = mock(() => Promise.resolve(null));
// Store originals
const originalFetch = globalThis.fetch;
const originalNodeEnv = process.env.NODE_ENV;
// Spies
let getQueryIdSpy: Mock<typeof runtimeQueryIds.getQueryId>;
let refreshSpy: Mock<typeof runtimeQueryIds.refresh>;
// Mock response helper
function mockResponse(body: unknown, options: { status?: number; ok?: boolean } = {}) {
const status = options.status ?? 200;
const ok = options.ok ?? (status >= 200 && status < 300);
return {
ok,
status,
text: () => Promise.resolve(typeof body === "string" ? body : JSON.stringify(body)),
json: () => Promise.resolve(body),
} as Response;
}
// Valid cookies for tests
const validCookies = {
authToken: "test-auth-token",
ct0: "test-ct0",
cookieHeader: "auth_token=test-auth-token; ct0=test-ct0",
source: "test",
};
describe("XClient", () => {
beforeAll(() => {
getQueryIdSpy = spyOn(runtimeQueryIds, "getQueryId").mockImplementation(mockGetQueryId);
refreshSpy = spyOn(runtimeQueryIds, "refresh").mockImplementation(mockRefresh);
});
afterAll(() => {
getQueryIdSpy.mockRestore();
refreshSpy.mockRestore();
if (originalNodeEnv !== undefined) {
process.env.NODE_ENV = originalNodeEnv;
} else {
delete process.env.NODE_ENV;
}
});
beforeEach(() => {
mockGetQueryId.mockReset();
mockRefresh.mockReset();
process.env.NODE_ENV = "test";
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
it("example test", async () => {
const client = new XClient({ cookies: validCookies });
globalThis.fetch = mock(() => Promise.resolve(mockResponse({ data: {} })));
const result = await client.someMethod();
expect(result.success).toBe(true);
});
});Runtime Query ID Store Test Pattern
// @ts-nocheck - Test file with fetch mocking
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { createRuntimeQueryIdStore, type RuntimeQueryIdStore } from "./runtime-query-ids";
describe("runtime-query-ids", () => {
let store: RuntimeQueryIdStore;
let cachePath: string;
let testDir: string;
beforeEach(async () => {
testDir = await mkdtemp(path.join(tmpdir(), "xfeed-test-"));
cachePath = path.join(testDir, "query-ids-cache.json");
});
afterEach(async () => {
store?.clearMemory();
if (testDir) {
await rm(testDir, { recursive: true, force: true }).catch(() => {});
}
});
it("reads from cache", async () => {
const snapshot = {
fetchedAt: new Date().toISOString(),
ttlMs: 86400000,
ids: { CreateTweet: "abc123" },
discovery: { pages: ["https://x.com"], bundles: ["bundle.js"] },
};
await mkdir(path.dirname(cachePath), { recursive: true });
await writeFile(cachePath, JSON.stringify(snapshot));
store = createRuntimeQueryIdStore({ cachePath });
const info = await store.getSnapshotInfo();
expect(info?.snapshot.ids.CreateTweet).toBe("abc123");
});
it("fetches from network", async () => {
const mockFetch = mock((url: string) => {
if (url.includes("x.com")) {
return Promise.resolve({
ok: true,
text: () => Promise.resolve(
'<script src="https://abs.twimg.com/responsive-web/client-web/bundle.js"></script>'
),
});
}
if (url.includes("bundle.js")) {
return Promise.resolve({
ok: true,
text: () => Promise.resolve(
'e.exports={queryId:"abc123",operationName:"CreateTweet"}'
),
});
}
return Promise.resolve({ ok: true, text: () => Promise.resolve("") });
});
store = createRuntimeQueryIdStore({
cachePath,
fetchImpl: mockFetch as typeof fetch,
});
const info = await store.refresh(["CreateTweet"], { force: true });
expect(info?.snapshot.ids.CreateTweet).toBe("abc123");
});
});Cookie Extraction Test Pattern
// @ts-nocheck - Test file with module mocking
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
// Mutable mock implementation
let mockImpl = () => Promise.resolve({ cookies: [], warnings: [] });
// Mock BEFORE importing
mock.module("@steipete/sweet-cookie", () => ({
getCookies: () => mockImpl(),
}));
// NOW import the module
const { resolveCredentials } = await import("./cookies");
// Helper to set mock cookies
function setMockCookies(cookies: Array<{ name: string; value: string; domain: string }>) {
mockImpl = () => Promise.resolve({ cookies, warnings: [] });
}
describe("cookies", () => {
beforeEach(() => {
mockImpl = () => Promise.resolve({ cookies: [], warnings: [] });
});
it("extracts X cookies", async () => {
setMockCookies([
{ name: "auth_token", value: "token123", domain: ".x.com" },
{ name: "ct0", value: "csrf456", domain: ".x.com" },
]);
const result = await resolveCredentials();
expect(result.authToken).toBe("token123");
expect(result.ct0).toBe("csrf456");
});
});Preload Pattern for Mock Isolation
When mock.module() would pollute other test files, use a preload file to isolate mocks.
Problem: mock.module() persists across test files - mock.restore() does NOT reset it.
Solution: Create a preload file and run the test with --preload:
1. Create `check.test.preload.ts`:
import { mock } from "bun:test";
// Export mutable mock implementations
export let mockResolveCredentialsImpl = () => Promise.resolve({ cookies: {}, warnings: [] });
export let mockGetCurrentUserImpl = () => Promise.resolve({ success: true, user: {} });
// Helper to update mocks from tests
export function setMockResolveCredentials(impl: typeof mockResolveCredentialsImpl) {
mockResolveCredentialsImpl = impl;
}
export function resetMocks() {
mockResolveCredentialsImpl = () => Promise.resolve({ cookies: {}, warnings: [] });
// ... reset other mocks
}
// Mock modules BEFORE they're imported anywhere
mock.module("./cookies", () => ({
resolveCredentials: (options: unknown) => mockResolveCredentialsImpl(options),
}));
mock.module("@/api/client", () => ({
XClient: class MockXClient {
getCurrentUser() { return mockGetCurrentUserImpl(); }
},
}));2. Update test file to use preload helpers:
import { beforeEach, describe, expect, it } from "bun:test";
import { resetMocks, setMockResolveCredentials } from "./check.test.preload";
const { checkAuth } = await import("./check");
describe("check", () => {
beforeEach(() => {
resetMocks();
});
it("handles missing credentials", async () => {
setMockResolveCredentials(() => Promise.resolve({
cookies: { authToken: null, ct0: null },
warnings: [],
}));
const result = await checkAuth();
expect(result.ok).toBe(false);
});
});3. Run with preload:
bun test --preload ./src/auth/check.test.preload.ts src/auth/check.test.ts4. Update package.json to isolate tests:
{
"scripts": {
"test": "bun test src/api src/lib && bun test src/auth/cookies.test.ts && bun test --preload ./src/auth/check.test.preload.ts src/auth/check.test.ts"
}
}This ensures each test file gets the correct module implementations without pollution.
Multi-Step Mock Pattern (Video Upload)
For tests that require multiple fetch calls in sequence:
it("uploads video with polling", async () => {
const client = new XClient({ cookies: validCookies });
let callCount = 0;
globalThis.fetch = mock(() => {
callCount++;
if (callCount === 1) {
// INIT
return Promise.resolve(mockResponse({ media_id_string: "video-123" }));
}
if (callCount === 2) {
// APPEND
return Promise.resolve(mockResponse({}));
}
if (callCount === 3) {
// FINALIZE - return pending to trigger polling
return Promise.resolve(mockResponse({
processing_info: { state: "pending", check_after_secs: 0.001 },
}));
}
// STATUS check - return succeeded
return Promise.resolve(mockResponse({
processing_info: { state: "succeeded" },
}));
});
const result = await client.uploadMedia({
data: new Uint8Array([1, 2, 3]),
mimeType: "video/mp4",
});
expect(result.success).toBe(true);
expect(callCount).toBe(4); // INIT + APPEND + FINALIZE + STATUS
});GraphQL Response Patterns
X API responses have nested structures. Use these patterns:
// Tweet response
const tweetResponse = {
data: {
tweetResult: {
result: {
rest_id: "123456",
legacy: {
full_text: "Hello world!",
created_at: "Wed Oct 10 20:19:24 +0000 2018",
reply_count: 5,
retweet_count: 10,
favorite_count: 20,
conversation_id_str: "123456",
},
core: {
user_results: {
result: {
rest_id: "user123",
legacy: {
screen_name: "testuser",
name: "Test User",
},
},
},
},
},
},
},
};
// Timeline response
const timelineResponse = {
data: {
home: {
home_timeline_urt: {
instructions: [
{
type: "TimelineAddEntries",
entries: [
{
entryId: "tweet-123",
content: {
itemContent: {
tweet_results: {
result: { /* tweet structure */ },
},
},
},
},
],
},
],
},
},
},
};Related skills
FAQ
How do I mock fetch in Bun?
Save globalThis.fetch, replace it with a mock returning a Response-like object, and restore it in afterEach.
How do I isolate temp directories?
Use mkdtemp() per test rather than a shared temp directory to avoid race conditions.