# Testing applications and installable apps

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:

```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
```

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

| 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

- `--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

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

```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 effect | How to test it |
| --- | --- |
| Email | 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:

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:

```sh
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](https://cicadel.fauzaanu.com/en/dev/topics/component-ui/index.md#exact-authoring-and-verification-loop)
and [recorded verification](https://cicadel.fauzaanu.com/en/dev/sources/docs/component-verification.md).

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