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
Read https://cicadel.fauzaanu.com/en/0.2.8/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
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 istestserverand secure cookies are on. Change the returned value before you build the app to test other configurations.apptest.Newregisters 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.Checkis 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:
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.outInstall 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=2mand./.... 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
| Field | Contents |
|---|---|
version | 1, the report schema version |
tests | Outcomes of named tests and subtests |
packages | Package outcomes |
skipped | Count 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
--strictfails if any named test skips or if no named tests run. Use it for verification, not for-listor benchmark-only runs.--require-integrationsetsCICADEL_TEST_REQUIRE_INTEGRATION=1. Helpers that useapptest.RequireEnvthen fail instead of skipping when a prerequisite is missing.- Strict mode also catches ordinary
t.Skipcalls 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.
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
| API | What it does |
|---|---|
Request(t, method, path, body, headers) | Any method, headers or multipart form. Buffers at most 4 MiB and closes the body. |
Response.HTTP | The headers and status. |
Response.Body | The body as reusable bytes. |
Status, Header, Contains, Redirect, JSON | Assertions that report failures at the caller. |
DecodeJSON | Decodes 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.
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
- Create the database fixture first.
- Then create the app, client and server resources.
- 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,AssertCountandAssertMaxsupport 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 effect | How to test it |
|---|---|
Inject mail.Capture as the mail transport and assert Messages(). | |
| File storage | Inject storage.NewLocal(t.TempDir(), limit) and register its Close with cleanup. |
| Cache | Create a fresh cache.NewMemory per test. Its contents are not shared. |
| Management commands | Give management.Manager.Output a bytes.Buffer, run Manager.Run(t.Context(), args), then assert errors, output and saved state. |
| River workers | Test 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:
- Find existing tests and public APIs. Identify the behavior and the failure mode.
- Add a minimal regression test. Run it against the broken behavior to prove it fails.
- Make the change. Rerun the targeted test, then the 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.
- Check coverage for missed behavior. Add boundary, authorization, rollback and failure-path cases. A coverage percentage alone does not prove correctness.
- 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:
go tool cicadel ui build
go tool cicadel ui check --json- The source check runs offline. It compiles tests without running them.
manage.go checkseparately validates the registered libraries and assets.- Test component behavior with
apptestHTTP 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-librarymodule 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 installbefore the root suite. Tests never download it for you. TestComponentAuthoringGuideextracts 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.TestStarterTailwindchecks that the shipped stylesheet is real, up-to-date compiler output.
#Accounts, mail and logging
Run contrib/accounts as its own module:
ui buildandui check --json.go test -race ./...withCICADEL_TEST_DATABASE_URLset to the disposable PostgreSQL server.sh verify-host.sh.- 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.