udecode/better-convex · Archived

testing

Use when writing React hook tests, encountering test failures, flaky tests, cross-file contamination, or "Hook timed out" errors - Bun test patterns with proper spy cleanup

First seen Mar 12, 2026

Installation

$ npx skills add udecode/better-convex --skill testing

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from udecode/better-convex · top by installs.

npx skills add udecode/better-convex

Browse all from udecode/better-convex

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 390
License LICENSE
Default branch main
Open issues 2
Status Archived

Skill metadata

Parsed from SKILL.md frontmatter.

More metadata
skiller
{"source":".agents\/rules\/testing.mdc"}

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 12,663 B
  • docs SUMMARY.md 187 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 2 installs

SKILL.md

Bun Testing Patterns

Overview

Bun's test runner provides Jest-compatible API with TypeScript support and fast execution. Critical: Test globals (describe, it, expect, mock, spyOn, beforeEach, afterEach) are available globally via tooling/global.d.ts - no imports needed. mock.module() is process-global - use spyOn() instead.

Setup (First-Time Installation)

1. bunfig.toml

[test]
# Preload scripts execute BEFORE any test file
preload = ["./tooling/test-setup.ts"]

# Coverage exclusions
coveragePathIgnorePatterns = [
  "node_modules/**",
  "**/*.d.ts",
]

2. tooling/global.d.ts

/// <reference types="bun-types/test-globals" />

declare var mock: typeof import("bun:test").mock;
declare var spyOn: typeof import("bun:test").spyOn;

3. tooling/test-setup.ts

import { afterEach, expect, mock, spyOn } from "bun:test";
import { GlobalRegistrator } from "@happy-dom/global-registrator";
import * as matchers from "@testing-library/jest-dom/matchers";
import { cleanup } from "@testing-library/react";

// Make mock and spyOn globally available
(globalThis as any).mock = mock;
(globalThis as any).spyOn = spyOn;

// Register DOM globals synchronously
GlobalRegistrator.register();

// Ensure document.body exists
if (global.document && !global.document.body) {
  const body = global.document.createElement("body");
  global.document.documentElement.appendChild(body);
}

// Extend Bun's expect with Testing Library matchers
expect.extend(matchers);

// Cleanup after each test
afterEach(() => {
  cleanup();
});

4. Dependencies

bun add -d @happy-dom/global-registrator @testing-library/react @testing-library/jest-dom

When to Use

  • Writing new tests for React hooks
  • Debugging test failures, especially when tests pass individually but fail in full suite
  • Fixing cross-file contamination ("test passes alone, fails with others")
  • Encountering "Hook timed out", race conditions, or flaky tests

Quick Reference

Pattern Use Case Example
No imports needed Test globals describe, it, expect, mock, spyOn are global
toMatchObject(array) Array partial match Checks properties exist, allows extras
toEqual() Exact match Validates complete structure
expect(val as any) Type mismatch Cast actual value, not expected
mock() not jest.fn() Create mock function Bun test API
spyOn() + afterEach Mock with cleanup Always spy.mockRestore()
renderHook() + act() Test hooks Wrap state changes in act()
void act() Prevent warnings Use with sync click/change events
ReturnType<typeof mock> Type mock variables let mockFn: ReturnType<typeof mock>
ReturnType<typeof spyOn> Type spy variables let spy: ReturnType<typeof spyOn>

Core Patterns

Test File Structure

// NO bun:test imports needed - globals are available
import { act, renderHook } from "@testing-library/react";
import * as apiModule from "@/lib/api"; // Import as namespace for spyOn

describe("HookName", () => {
  let mockFunction: ReturnType<typeof mock>;
  let functionSpy: ReturnType<typeof spyOn>;

  beforeEach(() => {
    mockFunction = mock();
    functionSpy = spyOn(apiModule, "functionName").mockImplementation(
      mockFunction
    );
    mockFunction.mockResolvedValue(defaultResponse);
  });

  afterEach(() => {
    // CRITICAL: Always restore spies
    functionSpy.mockRestore();
  });

  it("should do something", async () => {
    // Test implementation
  });
});

Avoiding Cross-File Contamination

Problem: mock.module() is process-global. If fileA.test.ts uses mock.module('@/lib/api'), it contaminates fileB.test.ts.

Solution: Use spyOn() instead of mock.module().

❌ WRONG - Causes Cross-Contamination

// This globally mocks the module for ALL test files
mock.module("@/lib/api", () => ({
  fetchData: mock(),
}));

✅ CORRECT - File-Scoped Mocking

import * as apiModule from "@/lib/api";

describe("MyHook", () => {
  let mockFetchData: ReturnType<typeof mock>;
  let fetchDataSpy: ReturnType<typeof spyOn>;

  beforeEach(() => {
    mockFetchData = mock();
    fetchDataSpy = spyOn(apiModule, "fetchData").mockImplementation(
      mockFetchData
    );
    mockFetchData.mockResolvedValue({ data: "test" });
  });

  afterEach(() => {
    fetchDataSpy.mockRestore();
  });

  it("fetches data", async () => {
    const { result } = renderHook(() => useMyHook());

    await act(async () => {
      await result.current.fetch();
    });

    expect(mockFetchData).toHaveBeenCalled();
  });
});

Key differences:

  1. Import module as namespace: import * as apiModule from './api'
  2. Create spies in beforeEach: spyOn(apiModule, 'function')
  3. Always mockRestore() in afterEach
  4. Use mock variables in assertions: expect(mockFn) not expect(apiModule.fn)

When mock.module() Is Allowed

Use mock.module() only for infrastructure every test needs, configured in a preload, or for a module that will never be imported as real code elsewhere in the same Bun process. It mutates process-global module state and may preserve unmocked real exports while overriding only returned keys. The result can look real while a few exports are poisoned.

Do not solve one contaminated test by lowering global concurrency, changing CI, or adding runtime dependency-indirection files solely for tests. Prefer a lower-level contract, a file-scoped spy, or a different integration harness.

mockReset() Destroys Implementations

mockReset() clears calls and the implementation. Restore defaults immediately:

const fetchValue = mock(async () => "default");

beforeEach(() => {
  fetchValue.mockReset();
  fetchValue.mockResolvedValue("default");
});

Use mockClear() when only call history should reset.

Module-Scope Environment Values Need Fresh Imports

If a subject reads an environment flag at module scope, changing the mock after the static import cannot affect it. Set a mutable environment mock first, then dynamically import the subject. Add a deterministic cache-busting import query only when Bun otherwise reuses the module. Avoid random cache keys in committed tests when an incrementing case id works.

Reproducing Order-Dependent Failures

If a test passes alone and fails in the suite, run likely contaminators before the victim in one command:

bun test likely-contaminator.test.ts victim.test.ts

Record the minimal ordering that fails. Inspect module keys, critical function types, and resolved default values behind a temporary opt-in debug flag. Remove debug output after the owner is proven.

Testing React Hooks

import { act, renderHook } from "@testing-library/react";

it("updates state correctly", async () => {
  const { result } = renderHook(() => useCustomHook());

  await act(async () => {
    await result.current.fetchData();
  });

  expect(result.current.data).toEqual(expectedData);
  expect(result.current.loading).toBe(false);
});

// Use void to prevent unused promise warnings
void act(() => getByText("button").click());

Custom wrapper pattern for context providers:

const createWrapper = (props) => ({ children }: any) => (
  <Provider {...props}>{children}</Provider>
);

const wrapper = createWrapper({ value: 'test' });
const { result } = renderHook(() => useCustomHook(), { wrapper });

Testing Async Errors

it("handles async errors", async () => {
  mockFetch.mockRejectedValue(new Error("Network error"));

  const { result } = renderHook(() => useCustomHook());

  await act(async () => {
    try {
      await result.current.fetchData();
    } catch (error) {
      expect(error).toEqual(new Error("Failed to load"));
    }
  });

  expect(result.current.error).toBe("Failed to load");
});

Matcher Selection

// ✅ Array partial matching
expect(children).toMatchObject([{ text: "one" }, { text: "two" }]);

// ✅ Exact matching
expect(result).toEqual({ data: "test" });

// ✅ Cast actual value for type mismatches
expect(node as any).toEqual({ text: "one" });

// ❌ Don't cast expected value
expect(children).toEqual([{ text: "one" }] as any);

// ❌ Don't use toMatchObject() for single objects (won't catch extra props)
expect(node).toMatchObject({ text: "one" });

Running Tests

# Run all tests
bun test

# Run specific file
bun test src/hooks/useMyHook.test.ts

# Watch mode
bun test --watch

# Coverage
bun test --coverage

# Bail on first failure
bun test --bail

Common Mistakes

Mistake Problem Fix
Importing from bun:test Unnecessary, globals available Remove imports
Using mock.module() Cross-file contamination Use spyOn() + afterEach cleanup
Forgetting afterEach cleanup Spies persist across tests Always spy.mockRestore()
Direct import for spyOn Can't spy on named exports import * as module
Forgetting act() React warnings, flaky tests Wrap state changes in act()
jest.fn() / jest.Mock Wrong framework Use mock() and ReturnType<typeof mock>
No type for mocks Type errors, autocomplete fails ReturnType<typeof mock>

Debugging Test Failures

Test passes alone, fails in suite

Symptom: bun test file.test.ts passes, bun test fails.

Cause: Cross-file contamination from mock.module().

Fix:

  1. Search for mock.module() calls
  2. Refactor to spyOn() pattern with afterEach cleanup

"Expected to be called but it was not called"

Cause: Wrong mock variable or wrong function name.

Fix:

  1. Verify spy setup: spyOn(module, 'correctFunctionName')
  2. Check assertions use mock variable: expect(mockFn) not expect(module.fn)

"Hook timed out after 5000ms"

Cause: Missing await, unresolved promise.

Fix:

  1. Ensure all async operations are awaited
  2. Check mock returns resolved promises: mockResolvedValue()
  3. Increase timeout if needed: it('name', fn, 10000)

Red Flags - Cross-Contamination Risk

  • Using mock.module() outside of preload scripts
  • Importing modules directly instead of as namespace for spyOn
  • Missing afterEach() with mockRestore() calls
  • Tests passing individually but failing in full suite

All indicate cross-file contamination. Refactor to spyOn pattern.

Implementation Checklist

For each new hook test file:

  • NO imports from bun:test - globals are available
  • Import renderHook, act from @testing-library/react when testing hooks
  • Import modules as namespace for spyOn: import * as module
  • Declare mock variables with ReturnType<typeof mock>
  • Declare spy variables with ReturnType<typeof spyOn>
  • Create spies in beforeEach with spyOn(module, 'fn')
  • Set default mock return values in beforeEach
  • CRITICAL: Restore spies in afterEach with spy.mockRestore()
  • Wrap React state changes in act()
  • Use mock variables in assertions, not module functions
  • Verify tests pass both individually and in full suite

TDD Workflow

  1. Red: Write failing test for hook behavior
  2. Green: Implement minimal code to pass test
  3. Refactor: Clean up implementation
  4. Repeat: Add next test case