cicadel
Documentation

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

text
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:

sh
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.out

Inside 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

go
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.

go
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.

go
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.Capture as the mail transport and assert Messages() contents.
  • Inject storage.NewLocal(t.TempDir(), limit) and register its Close with cleanup.
  • Create a fresh cache.NewMemory per test; its contents are not shared.
  • Give management commands a bytes.Buffer via management.Manager.Output, run Manager.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:

  1. Discover existing tests and public APIs; identify the behavior and failure mode.
  2. Add a minimal regression, then run it against the broken behavior to prove it fails.
  3. Implement the change and rerun the targeted test, followed by package tests.
  4. Run the full suite with race detection, real integration prerequisites, and strict reporting. Inspect failed and skipped outcomes. Run each nested module separately.
  5. Inspect coverage for missed behavior; add boundary, authorization, rollback, and failure-path cases. Coverage percentages alone do not establish correctness.
  6. 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.

↑

Type to search this version.