Skip to main content

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

ソース情報

リポジトリ
udecode/kitcn
ソースの最終更新活動
2026年7月22日 12:16
検出された SKILL.md の言語
英語
スター
450
フォーク
34

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
description
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
name
testing
metadata
{"skiller":{"source":".agents/rules/testing.mdc"}}
# 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 ```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 ```typescript /// <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 ```typescript 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 ```bash 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 ```typescript // 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 ```typescript // This globally mocks the module for ALL test files mock.module("@/lib/api", () => ({ fetchData: mock(), })); ``` #### ✅ CORRECT - File-Scoped Mocking ```typescript 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: ```typescript 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: ```bash 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 ```typescript 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: ```typescript const createWrapper = (props) => ({ children }: any) => ( <Provider {...props}>{children}</Provider> ); const wrapper = createWrapper({ value: 'test' }); const { result } = renderHook(() => useCustomHook(), { wrapper }); ``` ### Testing Async Errors ```typescript 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 ```typescript // ✅ 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 ```bash # 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"
GitHubで見る
この SKILL.md は非常に大きいため、SkillsMP では最初のセクションだけを表示しています。 GitHubで見る