Testing¶
Tomoda's test strategy has five layers, each with a clear cost / coverage tradeoff:
| Layer | Where | Runs against | Invocation |
|---|---|---|---|
| Unit | backend/internal/**/*_test.go, frontend/__tests__/ |
In-process mocks (testify/mock) + miniredis for Redis-backed code |
task test:unit / npm test |
| In-process integration | backend/tests/integration/, frontend/__tests__/integration/ |
miniredis (backend) / mocked fetch + WebSocket (frontend). No docker required. |
go test ./tests/integration/... / npm test |
| Real-stack integration | backend/**/*_test.go (Go tests with Integration in name) |
Real Postgres + Redis + MinIO from docker-compose.dev.yml |
task test:integration |
| Smoke | backend/cmd/server/main_test.go, backend/tests/smoke/, e2e/tests/{smoke,pages,auth}.spec.ts |
Wire DI graph + in-process gin router (backend); mocked-API Playwright (frontend) | go test ./cmd/server/... ./tests/smoke/... / ./scripts/run_e2e.sh |
| Simulation | backend/cmd/test/sim-workers/ |
Real DB + Redis, simulated user fleet | task test:simulation |
Unit tests¶
Backend¶
task test:unit # go test -v -short ./...
Unit tests live next to the code they test (foo.go ↔ foo_test.go). The
-short flag skips anything that needs a live database.
Test doubles: the project uses testify/mock — hand-rolled
Mock<Iface> structs with mock.Mock embedded. Repository interfaces
have these doubles in the same package (see e.g.
backend/internal/services/mock_*_test.go). There is no gomock /
mockgen step; if you need a new mock for an interface that doesn't
have one, add it by hand following the existing pattern.
Redis-backed code uses miniredis
(github.com/alicebob/miniredis/v2) instead of mocking the redis.Client
interface — that catches real pub/sub, SETNX, ZRANGEBYSCORE, and TTL
semantics that mocks miss. Pattern:
mr := miniredis.RunT(t) // auto-closes on test end
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
// pass rdb where the production code expects redis.UniversalClient / Cmdable
miniredis.RunT(t) gives a fresh server per test — no shared state.
Note that miniredis uses a fake clock for TTLs; call mr.FastForward(d)
to advance it when testing expiry behavior.
Frontend¶
cd frontend
npm test
Frontend unit tests are at frontend/__tests__/ and use Jest +
@testing-library/react-native. Coverage includes hooks
(useSearch, useLocalizedName), contexts (AuthContext,
FriendsContext), service clients (chatService, eventService,
friendService, discoveryService), pure utilities (date, location,
password validator), and a handful of UI primitives. Mock the network
boundary via jest.mock of the service module, or stub global.fetch
/ global.WebSocket for service-level tests.
In-process integration tests¶
cd backend && go test ./tests/integration/...
cd frontend && npm test # picks up __tests__/integration/ automatically
These wire multiple components together but don't require docker.
- Backend (
backend/tests/integration/, packageintegration): miniredis-backed flows that exercise contracts across packages. Examples: twowebsocket.Hubinstances exchanging messages viachat:event:*pub/sub with origin-dedup; threescheduler.Schedulerinstances electing a single leader and failing over;Workerqueue ownership;ChatServicecache hit/miss + trim; auth refresh-token full lifecycle. - Frontend (
frontend/__tests__/integration/):AuthContext+ login flow with a mockedfetch;chatServiceWS lifecycle (connect → message → reconnect → max-attempts → heartbeat); chat screen render-and-send with a mocked WebSocket.
These run on every go test ./... / npm test and are the best
catch-net for breakage from refactors that unit tests miss but a full
stack-up integration test would be too slow to debug.
Real-stack integration tests¶
task test:integration # go test -v -run Integration ./...
These spin up against the already-running local Docker stack —
Postgres, Redis, MinIO. Make sure task dev (or at least
docker-compose -f docker-compose.dev.yml up -d) is running first,
otherwise the connection setup in the test will fail.
Use this layer for things miniredis can't fake — real SQL behavior, JSONB serialization, H3 spatial queries, full GORM hooks, AWS SDK round-trips against MinIO.
Smoke tests¶
Backend¶
go test ./cmd/server/... ./tests/smoke/...
Fast checks that the binary's startup wiring is sound:
backend/cmd/server/main_test.go— table-driven test of the--moderesolver (full/multi-hub/async/api-hub/ws-hub) and a Wire DI smoke that calls the realInitializeAppagainst miniredis + sqlite to assert every top-level component is non-nil.backend/tests/smoke/— runtime-shape checks:Hub.Runstarts without panic (with + without Redis),/healthresponds, the mode matrix is coherent (api-hub ∪ ws-hub == multi-hub).
These don't exercise behavior — they catch infrastructure regressions (broken DI graph, missing health endpoint, mode misconfiguration).
Frontend (Playwright)¶
E2E tests live at /Users/zayaanm/workspace/tomoda/e2e/ (config:
e2e/playwright.config.ts). The Playwright suite is treated as smoke
— shallow, fast, broad — not deep flow verification:
e2e/tests/smoke.spec.ts— backend/health+ frontend/reach.e2e/tests/pages.spec.ts— each top-level route loads without console errors, with the backend API stubbed viapage.route('**/api/v1/**', ...).e2e/tests/auth.spec.ts— auth gate redirects unauthed users, login form renders the right fields, login submit posts and redirects out of/auth/login.
The driver script scripts/run_e2e.sh orchestrates the full stack:
- Detects Docker or Podman + a compose tool.
npm install+npx playwright install chromiumine2e/.- Brings up the full application stack with
docker compose up -d --build(usesdocker-compose.yml, the production-style compose file with backend + frontend containers built locally). - Polls
http://localhost:8080/healthuntil the backend is healthy (max ~60s). - Polls
http://localhost:8081until the frontend returns200 OK. - Runs
npx playwright test. - Optionally tears the stack down.
./scripts/run_e2e.sh
The report ends up at e2e/playwright-report/index.html. On CI,
playwright.config.ts enables 2 retries and forces workers: 1 for
deterministic runs.
Trace on first retry
trace: 'on-first-retry' means Playwright records a trace the second
time a test runs after failing. View it with
npx playwright show-trace path/to/trace.zip — far more useful than
raw logs.
Simulation tests¶
Simulation tests stress the system from the user side — simulated workers that move around, send messages, create events, and trigger discovery / presence updates. They're how we shake out load-related bugs that don't show up under single-user testing.
task test:simulation
What it does:
- Runs
backend/cmd/test/data— clears the DB tables + Redis keys and re-seeds fresh test data (~20+ users, events, chats). - Runs
backend/cmd/test/sim-workers— spins up the worker pool that simulates user behavior.Ctrl+Cto stop.
The infra (Postgres, Redis, MinIO) must already be up via task dev
or task docker:up. The script doesn't bring up its own dependencies.
Coverage¶
task test:coverage
Runs the full backend suite with -coverprofile=coverage.out, then
renders an HTML report at backend/coverage.html and opens it in your
browser. Treat the number as a guide, not a target. Many
integration paths are covered indirectly by the in-process integration
tests, smoke tests, real-stack E2E, and simulation — none of which
contribute to Go's coverage profile for the package under test.
What runs in CI¶
task ci is the umbrella target — lint, vet, format,
typecheck, test, build. Run it locally before pushing if you
want a fast confidence check that CI will be happy.
CI itself is Cloud Build for image builds (see
Cloud Build); the full test
pipeline (task ci) is typically run from a separate CI system or
locally pre-push — verify the actual CI configuration with the team.
Playwright in CI
The Playwright suite needs npm install && npx playwright install
before running. The driver script scripts/run_e2e.sh handles
both. Headless Chromium binaries are not committed; CI must
install them.