Testing ¶
Cicadel uses Go’s testing framework. There is no second test language or hidden
registry: tests are ordinary *_test.go functions, dependencies are explicit,
and the same tests work with go test, editors, race detection, coverage, fuzzing,
and AI coding agents. apptest removes setup repetition without bypassing the
application’s security or lifecycle. The PostgreSQL fixture lives in
github.com/fauzaanu/cicadel/apptest/postgres.
Ask your agent ¶
Read https://cicadel.fauzaanu.com/en/dev/topics/testing/index.md.
Add application tests for [feature] using the matching Cicadel apptest API.
Exercise real routes, validation, permissions and CSRF. Use an isolated
PostgreSQL fixture for database behavior. Run the tests and report any skips.Run, discover, and diagnose ¶
From a project directory with the matching cicadel CLI installed:
go test -list . ./...
cicadel test --report test-results.json -- -race -coverprofile=coverage.out ./...
cicadel test --strict --require-integration --report test-results.json -- -race ./...
cicadel test -- -json ./... > test-events.jsonl
go test -count=1 -run '^TestArticle$/^other_author$' ./apps/articles
go tool cover -func=coverage.outInside the framework repository, use go run ./cmd/cicadel test .... Install a
project’s matching CLI version with go install github.com/fauzaanu/cicadel/cmd/cicadel@VERSION.
The wrapper defaults to -count=1 -timeout=2m and ./...; explicit Go flags can
override count/timeout. Put framework flags before -- and Go flags after it.
The wrapper streams output, returns a failing exit status on build/test failures,
and atomically replaces the requested JSON report, including on failures.
The report directory must already exist. version: 1 identifies the report
schema; tests contains named test/subtest outcomes, packages contains package
outcomes, and skipped counts named skips. Parent tests and subtests each have
records; their count is not a count of independent scenarios. Preserve JSON events
as well when detailed failure logs are needed. Test output can contain supplied
payloads, so use synthetic data and treat artifacts accordingly.
--strict additionally fails if any named test skips or no named tests execute.
It is intended for verification, not -list or benchmark-only discovery.
--require-integration sets CICADEL_TEST_REQUIRE_INTEGRATION=1; helpers using
apptest.RequireEnv fail instead of skipping missing prerequisites. Strict mode
also catches ordinary t.Skip in application tests. An absent database must never
be reported as verified integration coverage.
Like go test, ./... covers the current module, not nested modules. Run the
command separately in each module (including contrib/migrations). Go fuzzing
usually selects one package and one fuzz target; use standard Go flags for that.
Application and HTTP tests ¶
func TestIndex(t *testing.T) {
t.Parallel()
app := apptest.New(t, apptest.Settings(), []cicadel.App{notes.New()})
client := apptest.NewClient(app)
client.Get(t, "/notes/").Status(t, 200).JSON(t, `{"app":"notes"}`)
}apptest.Settings() returns fresh, environment-independent settings with a
testserver allowed host and secure cookies enabled. Mutate the returned value
before construction for configuration-specific tests. apptest.New registers
cleanup before startup, runs the real application startup/checks, uses the test’s
context for its lifetime, and closes with an independent bounded context.
Construct fresh app factories and dependencies per test. apptest.Check remains
the two-instance lifecycle conformance smoke test for installable app authors.
Each NewClient has an independent cookie jar. Requests run in process through
the supplied handler, using https://testserver, without a listener or network
access. Redirects remain responses so tests can assert status and location.
Cookies respect path, domain, expiry, and secure flags. Authentication, CSRF,
request limits, host checks, and middleware are not disabled. Explicit request
hosts can be changed to test host validation; other URL origins are rejected.
alice, bob := apptest.NewClient(app), apptest.NewClient(app)
alice.PostForm(t, "/login", url.Values{
"username": {"alice"}, "password": {"test password"},
"csrf_token": {tokenObtainedFromLoginPage},
}).Redirect(t, http.StatusSeeOther, "/")
bob.Get(t, "/private/").Status(t, http.StatusUnauthorized)
alice.JSON(t, "POST", "/api/notes", map[string]string{"title":"hello"}).Status(t, 201)Use Request(t, method, path, body, headers) for arbitrary methods, headers, or
multipart forms. It buffers at most 4 MiB and closes the body. Response.HTTP
exposes headers/status; Response.Body holds reusable bytes. Status, Header,
Contains, Redirect, and JSON report failures at the caller. DecodeJSON
decodes into a typed destination and rejects trailing values. JSON assertions
ignore whitespace and object-key ordering; array order and number spelling stay
significant (1 differs from 1.0), with no float64 precision loss.
Client.Do accepts a standard *http.Request for cancellation, explicit host,
and large-body tests; callers must close the response body. It does not emulate
a browser, TLS negotiation, streaming, network timeouts, or wire-level behavior
such as HEAD body suppression. Use Go’s httptest.NewServer / NewTLSServer
for those transport tests. Browser cookie policies such as SameSite are outside
this in-process client’s scope.
PostgreSQL isolation and query budgets ¶
Set CICADEL_TEST_DATABASE_URL to a dedicated test PostgreSQL server whose role
has CREATEDB. This is an administrator connection, not an application database
that the fixture truncates. The fixture creates a random database from template0
and drops only that database at cleanup. PostgreSQL 13+ is required for forced
cleanup. It never falls back to DATABASE_URL, synchronizes a schema, or invokes
migrations implicitly.
func TestCreateArticle(t *testing.T) {
t.Parallel()
fixture := postgres.New(t)
// Apply the application's real migrations explicitly using fixture.SQL/URL.
// Seed typed fixtures after migration, checking every returned error.
db, queries := postgres.Capture(fixture.DB)
s := apptest.Settings()
s.DatabaseURL = fixture.URL
app := apptest.New(t, s, []cicadel.App{articles.New(db)})
client := apptest.NewClient(app)
queries.Reset() // exclude startup/setup from the request budget
client.Get(t, "/articles/").Status(t, 200)
queries.AssertMax(t, 3)
}Create the database first, then app/client/server resources. Go’s cleanup runs in
reverse registration order, letting workers and application pools close before
the fixture drops the database. Give every additional connection (session stores,
workers, migrations) fixture.URL. Do not reuse database fixtures across parallel
tests. Process termination can prevent cleanup; disposable CI servers are still
appropriate. Cleanup failures fail the test.
postgres.Transaction(t, db) starts a transaction and rolls it back on cleanup.
Inject that transaction into every code path under test; do not commit it.
Connections opened elsewhere cannot see uncommitted fixtures. Use a fresh database
for tests involving commits, concurrent connections, constraints, or workers.
postgres.Capture(db) returns a private GORM session and a concurrency-safe query
recorder without changing the original session’s logger. Inject the returned
session. Snapshot, Reset, AssertCount, and AssertMax support exact/budget
assertions. Failed statements count. SQL text and arguments are not retained.
Raw database/sql calls, other sessions, and transaction control statements are
outside this recorder; it is a GORM query budget, not a global SQL audit. Wait for
background work before assertions and resets.
Fixtures and side effects ¶
Keep domain fixture functions in _test.go with typed inputs, explicit defaults,
and returned objects. Use fresh dependencies per test and t.Cleanup; avoid global
settings, global registries, or environment mutation in parallel tests.
Existing framework components already support observable side effects:
- Inject
mail.Captureas the mail transport and assertMessages()contents. - Inject
storage.NewLocal(t.TempDir(), limit)and register itsClosewith cleanup. - Create a fresh
cache.NewMemoryper test; its contents are not shared. - Give management commands a
bytes.Bufferviamanagement.Manager.Output, runManager.Run(t.Context(), args), and assert errors, output, and persisted state. - Test River worker logic with explicit dependencies; use a fresh PostgreSQL fixture for transactional enqueue/rollback and actual worker integration.
There is no implicit privileged login or automatic CSRF bypass. Test unauthorized, forbidden, invalid-input, and cross-user cases alongside successful workflows. Verify actual persistence and side effects, not just HTTP status.
AI authoring and verification contract ¶
Generated projects, apps, and installable packages include executable starter tests;
projects/packages also include TESTING.md. An agent should:
- Discover existing tests and public APIs; identify the behavior and failure mode.
- Add a minimal regression, then run it against the broken behavior to prove it fails.
- Implement the change and rerun the targeted test, followed by package tests.
- Run the full suite with race detection, real integration prerequisites, and strict reporting. Inspect failed and skipped outcomes. Run each nested module separately.
- Inspect coverage for missed behavior; add boundary, authorization, rollback, and failure-path cases. Coverage percentages alone do not establish correctness.
- Report exact commands and limitations. Never weaken assertions, disable security, auto-accept snapshots, or substitute mocks for a claimed database integration check.
Use deterministic values and synchronization instead of sleeps. Fuzz parsers, normalizers, routing, and validation using standard Go fuzz targets; preserve failing inputs in Go’s corpus. This system provides foundations for reliable agent-written tests, not a guarantee that generated tests are complete or correct.
Component verification ¶
Install the pinned Tailwind CLI once with go tool cicadel ui css install when using
the default CSS workflow. Use go tool cicadel ui build, then go tool cicadel ui check --json in generated
projects. The source check is offline and compiles tests without executing them;
manage.go check separately validates the actual registered libraries and assets.
Use apptest HTTP assertions and ordinary Go tests for component behavior. Render
with (ui.Renderer{}).Render(t.Context(), content) for focused checks; no separate
component test language or snapshot acceptance tool is required.
See the component authoring loop
and recorded verification. Run the independent
examples/component-library module separately. The generated-consumer test exercises
actual versioned module installation, explicit overrides and compiled operation
outside the checkout, without browser automation.
The framework’s Tailwind/scaffold documentation tests require that installed tool;
run go run ./cmd/cicadel ui css install before the root suite. Tests never download
it implicitly. TestComponentAuthoringGuide extracts the guide’s file-marked code
blocks into a fresh starter, generates markup/CSS, type-checks, and runs its HTTP,
escaping and contract tests under the race detector. TestStarterTailwind checks
that the shipped stylesheet is genuine up-to-date compiler output.
Accounts, mail and logging ¶
Run contrib/accounts separately: ui build, ui check --json, go test -race ./..., sh verify-host.sh, vet/build and vulnerability scanning. Set CICADEL_TEST_DATABASE_URL to the disposable PostgreSQL server. The root scaffold suite now tests startproject in a separate module; it requires the same integration prerequisites. SMTP tests use isolated loopback servers and test certificates, and logging alerts use an in-memory outbox. They do not deliver external email. See the workflow commands in .github/workflows/accounts.yml.