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에서 보기