원클릭으로
testing
Use when writing, reviewing, or improving tests for any package in the Anticapture monorepo. Covers unit, integration, and E2E tests.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Use when writing, reviewing, or improving tests for any package in the Anticapture monorepo. Covers unit, integration, and E2E tests.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Use for apps/api work: adding or changing REST controllers, services, repositories, mappers, clients, schema mapping, or API tests.
Use for apps/dashboard work: routes, features, shared components/hooks, styling, data-fetching wiring, and dashboard tests.
Use for apps/gateful work: REST aggregation of DAO APIs, Hono + zod-openapi routing, DAO source discovery (DAO_API_*), proxying, caching, circuit breaker, and gateway tests.
Use when preparing a PR — adding a changeset, choosing bump types, empty changesets, API-contract changes, or understanding the version/release flow.
Use when adding a new DAO to the Anticapture platform. Covers all five components — indexer, API, gateway, dashboard, and enum sync — with a step-by-step checklist.
Use when adding a new DAO to the Anticapture dashboard. Walks through enum registration, config creation, quorum label, icon setup, and wiring — with exact file paths and a worked example (FLUID).
| name | testing |
| description | Use when writing, reviewing, or improving tests for any package in the Anticapture monorepo. Covers unit, integration, and E2E tests. |
The monorepo uses two runners — check which one before writing test code:
| Package | Runner | Globals |
|---|---|---|
apps/api, apps/gateful | Vitest | vi.fn(), vi.useFakeTimers(), ... |
apps/dashboard | Jest | jest.fn(), jest.useFakeTimers(), ... |
apps/indexer | — | no test suite; verify with typecheck + lint |
Code examples below use Vitest (vi.*) syntax; in the dashboard, swap vi for jest
— the APIs are otherwise identical for everything this guide uses.
Follow the test pyramid strategy with three test types. Target ratio: ~70% unit, ~20% integration, ~10% E2E
Unit Tests (majority)
Integration Tests (moderate)
E2E Tests (few)
Prefer stubs and fakes over mocks to avoid brittle tests.
| Type | Use When | Example |
|---|---|---|
| Stub | Need fixed return values | { findAll: () => [mockData] } |
| Fake | Need working simplified implementation | InMemoryRepository |
| Mock | Need to verify a call was made (use sparingly) | vi.fn() with toHaveBeenCalled |
// ❌ Avoid: mock that verifies implementation
expect(repository.findById).toHaveBeenCalledWith("123");
// ✅ Prefer: stub that enables behavior testing
const stub = { findById: () => mockAccount };
const result = await service.getAccount("123");
expect(result.name).toBe("vitalik.eth");
Exception: Use mocks when the call itself IS the behavior (e.g., verifying an event was emitted, an email was sent).
Structure every test using AAA for readability and consistency. Do not write // Arrange, // Act, // Assert comments — use blank lines to separate sections visually.
it("should calculate delegation percentage", async () => {
const mockData = [createMockRow({ delegated: 100n, total: 1000n })];
stubRepository.getDelegationPercentage.mockResolvedValue(mockData);
const result = await service.execute({ dao: "uni" });
expect(result[0].percentage).toBe(10);
});
Write tests for all new business logic. There is no minimum coverage enforced in CI.
lib/)Cover the happy path first, then actively try to break your code.
| Category | Examples |
|---|---|
| Empty values | null, undefined, [], "", {} |
| Zeros and boundaries | 0, 1, -1, MAX_INT, overflow |
| Divisions | Division by zero, percentages > 100% |
| Dates | Future timestamps, distant past, midnight edge cases |
| Strings | Unicode, whitespace, special characters |
| Concurrency | Missing data, unexpected order |
Write tests that produce the same result every time, in any environment, in any order.
Date.now())// ✅ Deterministic - mock the time
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2025-01-15T00:00:00Z"));
});
afterEach(() => {
vi.useRealTimers();
});
Never use expect to check if a property exists or to validate only part of an object. Always assert the entire object using toEqual.
// ❌ Avoid: partial assertions
expect(result).toHaveProperty("interactionCount");
expect(result).toMatchObject({ interactions: [] });
// ✅ Prefer: full object assertion
expect(result).toEqual({ interactionCount: 0, interactions: [] });
When the same expected value is used across multiple tests, extract it as a shared const:
const EMPTY_INTERACTIONS = { interactionCount: 0, interactions: [] };
Controller tests must use the real application stack — no mocks for services, repositories, or database layers. External HTTP calls (third-party APIs) are intercepted and controlled via MSW.
// ✅ Controller test: real Hono app, real DB, MSW for external HTTP
import { testClient } from "hono/testing";
import { setupServer } from "msw/node";
import { app } from "../app";
const server = setupServer(/* MSW handlers */);
const client = testClient(app);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
it("should return interactions for an account", async () => {
const res = await client.account[":address"].interactions.$get({
param: { address: "0x123" },
query: { dao: "uni" },
});
expect(res.status).toBe(200);
expect(await res.json()).toEqual({ interactionCount: 1, interactions: [...] });
});
Never inject fake services or repositories into the controller for testing — only use MSW to control external boundaries.
All test code must be correctly typed. Never use as any, as unknown, or other unsafe casts.
In unit tests for services and repositories, implement the actual TypeScript interface to build fakes and stubs:
// ❌ Avoid: casting to bypass types
const fakeRepo = { findAll: () => [] } as any;
// ✅ Prefer: implement the interface
import type { AccountRepository } from "../repositories/account";
const fakeRepo: AccountRepository = {
findAll: async () => [],
findById: async () => null,
};
import { testClient } from "hono/testing";
import { app } from "../app";
const client = testClient(app);
it("should return proposals", async () => {
const res = await client.proposals.$get({
query: { dao: "uni", limit: "10" },
});
expect(res.status).toBe(200);
const data = await res.json();
expect(data.proposals).toHaveLength(10);
});
Use PGLite for integration tests.
Use MSW (Mock Service Worker) to intercept HTTP requests:
import { setupServer } from "msw/node";
import { http, HttpResponse } from "msw";
const server = setupServer(
http.get("https://api.coingecko.com/simple/price", () => {
return HttpResponse.json({ uniswap: { usd: 7.5 } });
}),
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// factories/proposal.ts
export function createProposal(overrides?: Partial<Proposal>): Proposal {
return {
id: randomId(),
dao: "uni",
title: "Test Proposal",
status: "active",
createdAt: new Date(),
...overrides,
};
}
async function seedTestData() {
const account = createAccount({ address: "0x123" });
const proposal = createProposal({ dao: "uni" });
const vote = createVote({ proposalId: proposal.id, voter: account.address });
await db.insert(accounts).values(account);
await db.insert(proposals).values(proposal);
await db.insert(votes).values(vote);
return { account, proposal, vote };
}