cicadel

Testing

Cicadel tests are ordinary Go tests in *_test.go files. The apptest package removes setup repetition without bypassing your application’s security or lifecycle.

There is no second test language and no hidden registry. Dependencies are explicit. The same tests work with go test, editors, race detection, coverage, fuzzing and AI coding agents. 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.

#Write your first test

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 settings that ignore the environment. The allowed host is testserver and secure cookies are on. Change the returned value before you build the app to test other configurations.
  • apptest.New registers cleanup before startup. It runs the real application startup and checks, and uses the test’s context for its lifetime. It closes the app with a separate, bounded context.
  • Build fresh app factories and dependencies in every test.
  • apptest.Check is the two-instance lifecycle smoke test for installable app authors.

#Run, discover and diagnose

Run these from a project directory with the matching cicadel CLI installed:

Terminal
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

Install the project’s matching CLI with go install github.com/fauzaanu/cicadel/cmd/cicadel@VERSION. Inside the framework repository, use go run ./cmd/cicadel test ... instead.

How cicadel test behaves:

  • Framework flags go before --. Go flags go after it.
  • It defaults to -count=1 -timeout=2m and ./.... Your own Go flags can override the count and timeout.
  • It streams output and exits with a failure status on build or test failures.
  • It atomically replaces the JSON report, even when tests fail. The report directory must already exist.

#The JSON report

FieldContents
version1, the report schema version
testsOutcomes of named tests and subtests
packagesPackage outcomes
skippedCount of named skips

Parent tests and subtests each get a record. So the count is not a count of independent scenarios. Keep the JSON events too when you need detailed failure logs.

Warning

Test output can contain the payloads you supply. Use synthetic data and treat reports and logs accordingly.

#Strict mode and integration

  • --strict fails if any named test skips or if no named tests run. Use it for verification, not for -list or benchmark-only runs.
  • --require-integration sets CICADEL_TEST_REQUIRE_INTEGRATION=1. Helpers that use apptest.RequireEnv then fail instead of skipping when a prerequisite is missing.
  • Strict mode also catches ordinary t.Skip calls in application tests.

A missing database must never count as verified integration coverage.

#Nested modules and fuzzing

Like go test, ./... covers only the current module. Run the command separately in each nested module, including contrib/migrations. Go fuzzing usually targets one package and one fuzz target. Use the standard Go flags for that.

#HTTP clients

Each apptest.NewClient has its own cookie jar. Requests run in process through your handler at https://testserver. There is no listener and no network access.

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)
  • Nothing is disabled. Authentication, CSRF, request limits, host checks and middleware all run.
  • Redirects stay responses, so you can assert the status and location.
  • Cookies respect path, domain, expiry and the secure flag.
  • You can set an explicit request host to test host validation. Other URL origins are rejected.

#Requests and assertions

APIWhat it does
Request(t, method, path, body, headers)Any method, headers or multipart form. Buffers at most 4 MiB and closes the body.
Response.HTTPThe headers and status.
Response.BodyThe body as reusable bytes.
Status, Header, Contains, Redirect, JSONAssertions that report failures at the caller.
DecodeJSONDecodes into a typed value and rejects trailing values.

JSON assertions ignore whitespace and object key order. Array order and number spelling still matter: 1 differs from 1.0. Numbers lose no float64 precision.

#Lower-level requests

Client.Do takes a standard *http.Request. Use it for cancellation, an explicit host or large bodies. You must close the response body.

The in-process client does not emulate a browser, TLS negotiation, streaming, network timeouts or wire-level behavior such as HEAD body suppression. Browser cookie policies such as SameSite are also out of scope. Use Go’s httptest.NewServer or NewTLSServer for those transport tests.

#PostgreSQL isolation and query budgets

Set CICADEL_TEST_DATABASE_URL to a dedicated test PostgreSQL server. Its role needs CREATEDB.

Warning

This is an administrator connection, not an application database for the fixture to truncate. The fixture creates a random database from template0 and drops only that database at cleanup.

The fixture needs PostgreSQL 13+ for forced cleanup. It never falls back to DATABASE_URL. It never syncs a schema or runs migrations for you.

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)
}

#Order and cleanup

  1. Create the database fixture first.
  2. Then create the app, client and server resources.
  3. Give every extra connection fixture.URL: session stores, workers and migrations.

Go runs cleanup in reverse registration order. So workers and application pools close before the fixture drops the database.

  • Do not share database fixtures between parallel tests.
  • Cleanup failures fail the test.
  • Killing the process can prevent cleanup. Disposable CI servers are still a good fit.

#Transactions

postgres.Transaction(t, db) starts a transaction and rolls it back at 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 that involve commits, concurrent connections, constraints or workers.

#Query budgets

postgres.Capture(db) returns a private GORM session and a concurrency-safe query recorder. It does not change the original session’s logger. Inject the returned session.

  • Snapshot, Reset, AssertCount and AssertMax support exact counts and budgets.
  • Failed statements count.
  • SQL text and arguments are not kept.
  • Wait for background work before you assert or reset.

The recorder is a GORM query budget, not a global SQL audit. It does not see raw database/sql calls, other sessions or transaction control statements.

#Fixtures and side effects

Keep domain fixture functions in _test.go files. Give them typed inputs and explicit defaults, and return the objects they create. Use fresh dependencies per test and t.Cleanup. Avoid global settings, global registries and environment changes in parallel tests.

Framework components already expose side effects you can assert:

Side effectHow to test it
EmailInject mail.Capture as the mail transport and assert Messages().
File storageInject storage.NewLocal(t.TempDir(), limit) and register its Close with cleanup.
CacheCreate a fresh cache.NewMemory per test. Its contents are not shared.
Management commandsGive management.Manager.Output a bytes.Buffer, run Manager.Run(t.Context(), args), then assert errors, output and saved state.
River workersTest worker logic with explicit dependencies. Use a fresh PostgreSQL fixture for transactional enqueue, rollback and real worker integration.

There is no implicit privileged login and no automatic CSRF bypass. Test unauthorized, forbidden, invalid-input and cross-user cases next to the successful ones. Check what was actually saved and sent, not only the HTTP status.

#Workflow for AI agents

Generated projects, apps and installable packages include starter tests that run. Projects and packages also include TESTING.md. An agent should:

  1. Find existing tests and public APIs. Identify the behavior and the failure mode.
  2. Add a minimal regression test. Run it against the broken behavior to prove it fails.
  3. Make the change. Rerun the targeted test, then the 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. Check coverage for missed behavior. Add boundary, authorization, rollback and failure-path cases. A coverage percentage alone does not prove correctness.
  6. Report the exact commands and limits. Never weaken assertions, disable security, auto-accept snapshots or use mocks in place of a claimed database integration check.

Use deterministic values and synchronization instead of sleeps. Fuzz parsers, normalizers, routing and validation with standard Go fuzz targets. Keep failing inputs in Go’s corpus.

These tools help agents write reliable tests. They do not guarantee that generated tests are complete or correct.

#Components

With the default CSS workflow, install the pinned Tailwind CLI once with go tool cicadel ui css install. Then, in generated projects:

Terminal
go tool cicadel ui build
go tool cicadel ui check --json
  • The source check runs offline. It compiles tests without running them.
  • manage.go check separately validates the registered libraries and assets.
  • Test component behavior with apptest HTTP assertions and ordinary Go tests.
  • For focused checks, render with (ui.Renderer{}).Render(t.Context(), content).

You need no separate component test language or snapshot tool. See the component authoring loop and recorded verification.

#In the framework repository

  • Run the independent examples/component-library module separately. Its generated-consumer test installs real versioned modules outside the checkout and checks explicit overrides and compiled operation, without browser automation.
  • The Tailwind and scaffold documentation tests need the installed Tailwind tool. Run go run ./cmd/cicadel ui css install before the root suite. Tests never download it for you.
  • TestComponentAuthoringGuide extracts the guide’s file-marked code blocks into a fresh starter. It generates markup and CSS, type-checks, and runs the HTTP, escaping and contract tests under the race detector.
  • TestStarterTailwind checks that the shipped stylesheet is real, up-to-date compiler output.

#Accounts, mail and logging

Run contrib/accounts as its own module:

  1. ui build and ui check --json.
  2. go test -race ./... with CICADEL_TEST_DATABASE_URL set to the disposable PostgreSQL server.
  3. sh verify-host.sh.
  4. Vet, build and vulnerability scanning.

The exact commands are in .github/workflows/accounts.yml.

The root scaffold suite tests startproject in a separate module. It needs the same integration prerequisites.

No test delivers external email. SMTP tests use isolated loopback servers and test certificates. Logging alerts use an in-memory outbox.

Type to search this version.