원클릭으로
testing
Testing methodology — the test pyramid, mock vs real, harness patterns, flakiness prevention, and discipline.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Testing methodology — the test pyramid, mock vs real, harness patterns, flakiness prevention, and discipline.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Visual-design discipline for forge frontends — brief the work before building (never invent an aesthetic without user input), declare the design system before coding, lean on the component library, use restrained color/type systems, never hand-draw complex SVGs, and verify visually before declaring done.
Write Next.js frontends — generated hooks, component library, Tailwind v4, visual verification, and Connect RPC clients.
Outbound boundary translators. One adapter per third-party system / queue / storage backend; narrow interface, vendor-neutral callers.
Write Connect RPC handlers — proto-driven codegen, the thin-translation handler pattern (validate, extract auth, convert proto↔internal, call service, wrap errors via `svcerr.Wrap`), middleware, and testing. Business logic lives in `internal/handlers/<svc>/contract.go`, never in handlers.
Forge project conventions and architecture — project structure, generated vs hand-written code, the generate pipeline, proto annotations, contracts, wiring, and naming.
Use-case orchestrators that compose two or more adapters/services. Deps are interfaces only — designed for unit tests with all-mock collaborators.
| name | testing |
| description | Testing methodology — the test pyramid, mock vs real, harness patterns, flakiness prevention, and discipline. |
| emit | both |
Structure tests in layers, with volume decreasing as scope increases:
The pyramid shape matters. Many units, fewer integrations, a small e2e set. Inverting it — lots of e2e, few units — makes the suite slow and brittle, and failures point at the wrong layer.
This is the most critical testing decision. Get it wrong and tests either break constantly or miss real bugs.
Mock at system boundaries, keep internals real. If two components live in the same service, test them together. If they cross a network boundary, mock the far side.
The boundary moves as scope widens — and the one rule that never bends is never mock the subject under test:
If you catch yourself mocking the subject — or mocking an internal collaborator in an integration/e2e test — stop: you've either picked the wrong tier or you're about to write a test that can't fail for the right reason.
Flaky tests erode trust. Follow these rules strictly:
sleep(2) to "wait for the worker" is always wrong. Poll with a timeout instead.Hard-won rules. Violating any of these is how a fast unit suite turns into a multi-minute CI hang.
Any orchestrator that performs I/O — spawns subprocesses, hits the network, calls a code-generation pipeline — MUST delegate argument validation to a pure helper. Tests of the argument logic call ONLY the helper; never the runner.
runX(args):
validated, err = validateXArgs(args) # pure: inputs → normalized values + error
if err: return err
... slow I/O follows ...
Test validateXArgs from a unit test. Never runX — the runner is an integration- or e2e-tier concern.
Anything that touches subprocesses, network, the filesystem outside a managed temp dir, time-based behavior, or external services belongs behind a tag/marker (Go build tags, pytest marks, Jest projects — whatever your runtime offers) so the default test command runs only fast unit tests with a tight timeout. Heavy tests opt-in.
Tests that hit a live external dependency (a real third-party sandbox, a deployed environment, a real credential) go one step further: gate them behind an explicit opt-in env var (e.g. RUN_LIVE_TESTS=1) and skip-with-a-reason when it's unset. The default suite stays hermetic and green in CI; the live test runs only when a human (or a dedicated CI job) asks for it. A live test that runs by default is a flaky test waiting to happen and a credential leak waiting to bite.
Diagnostics, golden files, and assertions over hash-map data MUST sort keys before formatting or comparing. Map iteration order is non-deterministic in most languages; tests that compare formatted strings against a fixture will flake intermittently. Sort first, format second.
The same applies to any logged diagnostic output you intend to grep in tests.
pkg/tddThe canonical entry point for table-driven tests in a forge project is the github.com/reliant-labs/forge/pkg/tdd library. Forge's scaffolders (forge new, forge add service, forge package new, forge generate) emit unit / contract test files (plus CRUD integration tests when entities exist) that already import it. Scaffolded per-RPC rows are self-destructing — WantErr: connect.CodeUnimplemented fails the moment the handler is implemented, demanding a real assertion in its place; pkg/tdd has no permissive any-outcome mode. Treat the helpers below as the default vocabulary for new test files; reach for hand-rolled for _, tc := range cases only when the shape doesn't fit.
| Helper | Use |
|---|---|
tdd.Case[Req, Resp] + tdd.TableRPC | unary Connect RPC tests (unit/integration/E2E) |
tdd.ContractCase + tdd.TableContract | internal/<pkg>/contract.go Service tests |
tdd.E2EClient(t, srv, factory) | httptest.Server → typed Connect client |
tdd.NewMock(opts...) | terse Forge MockService construction |
tdd.AssertConnectError, tdd.WithTimeout, tdd.SetupMockDB | standalone helpers |
See testing/patterns for copy-paste-ready templates.
forge test # all test levels
forge test unit # unit only
forge test integration # integration only
forge test e2e # e2e only
forge test --coverage # with coverage report
forge test -V # verbose; use when debugging failures
forge test --race # Go race detector
Before you stand up your own test infra, reach for the capabilities forge ships — agents routinely reinvent these:
forge up --env=<env>. This builds every service, brings up the compose-managed infra (Postgres, observability, …), and deploys each service to its declared target. Use it instead of hand-rolling kubectl apply / raw manifests / a bespoke docker-compose to get a stack under test. Once it's up, point real Connect clients at the running services.deploy block names its own K8sCluster (the cluster field is the kubectl context). forge up / forge deploy send each service to its own context, so a flow that spans clusters is just the normal deploy talking to multiple contexts. Don't script per-cluster kubectl --context juggling in your test — declare the targets and let forge route. Verify each context is reachable first (see the debug skill).pkg/testkit and the generated mock_gen.go. pkg/testkit provides the harness primitives (migrated test DB, authed contexts, claims options) plus a fixture builder and a scenario builder for composing multi-step setups. Per-contract mocks are generated into mock_gen.go — use those for collaborators rather than hand-writing stubs. See the pkg/tdd table above for the table-driven entry points that sit on top.pkg/authn.Policy; the jwt-auth pack ships a dev-auth bypass (a synthetic dev token, gated on a dev-mode flag) so most tests can skip the real token dance. But when the behavior under test IS the auth/authz path — token validation, role gating, tenant scoping — turn the bypass OFF and drive a real token / real claims. A test of the auth path that runs under the dev bypass proves nothing about production auth.Forge enforces the "isolate heavy tests" discipline via Go build tags. Anything that touches subprocesses, network, filesystem outside t.TempDir(), time-based behavior, or external services MUST live in:
*_integration_test.go with //go:build integration — DB-bound tests.*_e2e_test.go with //go:build e2e — full-stack flows.Default forge test runs only the fast unit tests with a tight -timeout 30s.
TestRunNewKindValidation/empty_becomes_service originally invoked the full runNew pipeline (network, filesystem, subprocesses) from a unit-test file. It hung tests for 99+ seconds and would have stalled CI indefinitely without an external timeout. Fix: extracted validateNewArgs (pure, in-memory) and rewrote the test to call only the helper. If you find yourself writing runX(cmd, args) from a _test.go file in a unit context, stop and extract the validator first.
testing/unit — hermetic, fast handler-level tests.testing/integration — real-DB tests behind //go:build integration.testing/e2e — full-stack flows behind //go:build e2e.testing/patterns — copy-paste-ready table-driven templates for the four most common test shapes.For Next.js / vite-spa / React frontends, this Go-flavored skill does NOT apply. Load the top-level frontend-testing skill instead — it covers Vitest + Testing Library patterns, the mockTransport() seam, and the four-state page coverage rule.