Testing
Running Tests Locally
Your app's specs live under tests/specs/ and run through the framework's built-in runner. There are two equal ways to drive it: the wheels CLI from your terminal, or the runner's URL from any browser or HTTP client — the CLI calls that same URL under the hood, so nothing about your tooling choice changes what runs.
You'll learn:
- Running the suite with the wheels CLI — and without it
- Filtering by directory or a single spec, in both paths
- How the test database behaves
- The common failure modes and their fixes
With the wheels CLI
Section titled “With the wheels CLI”wheels test detects your running dev server, reloads the app, hits the test runner, and prints pass/fail counts. No ceremony:
# Run the full suitewheels test
# Filter by directory (positional or --filter)wheels test modelswheels test --filter=controllers
# Pick a reporter — simple (default), json, tapwheels test --reporter=tap
# CI mode: emits GitHub Actions ::error annotations per failed/errored specwheels test --ciThe supported flags are --filter=<dir>, --reporter=<simple|json|tap>, --verbose / -v, --ci, --no-test-db (don't auto-swap to the <datasource>_test test database), and --base-path=<path> (URL prefix for subfolder-mounted apps — auto-derived from WHEELS_SUBPATH or set(subpath=...) when omitted). A positional argument acts as the filter. There's also --core (with --db=) for running the framework's own suite — see the framework-testing page.
Without the wheels CLI
Section titled “Without the wheels CLI”The runner is a URL. With your app running in the development environment — under CommandBox, your own IIS/nginx setup, anything — hit:
# Run everything (browser-friendly HTML report)http://localhost:8080/wheels/app/tests
# Machine-readable, and exactly what the CLI consumescurl "http://localhost:8080/wheels/app/tests?format=json"
# Filter by directory (dot-notation, rooted at your app's tests/ mapping)curl "http://localhost:8080/wheels/app/tests?format=json&directory=tests.specs.models"Keep the dev loop as: change a spec, refresh the URL. Two things to know:
- Development only. The
/wheels/*surfaces are gated by a development-only allowlist (since 4.0.4, #2903) — in any other environment they return 404. Run tests where the app runs asdevelopment(locally, or a CI job configured that way — see CI Integration). - The datasource is whatever the app is using. The
<datasource>_testauto-swap is a CLI convenience; the raw runner exercises the datasource your app is currently configured with. If you drive tests by URL routinely, point your development datasource at a database you're happy to havetests/populate.cfmrebuild — or keep populate idempotent (it should be anyway; see Fixtures & Test Data).
An unrecognized directory= value is ignored and the full suite runs instead (#3083) — if a filtered run looks suspiciously slow or broad, check the value against your tests/specs/ layout.
Filtering
Section titled “Filtering”| Method | With the wheels CLI | Without |
|--------|---------------------|---------|
| By directory | wheels test models | &directory=tests.specs.models |
| One category of many | wheels test --filter=controllers | &directory=tests.specs.controllers |
| Skip in source | xdescribe(...) / xit(...) — built into WheelsTest | same |
WheelsTest doesn't ship tag-based include/exclude filtering at this release; directory filtering plus xdescribe/xit in the spec source is the supported granularity.
Common failure modes
Section titled “Common failure modes”- No Java found (CLI path) — on non-brew installs the CLI refuses to start and points at Adoptium when it can't find a JVM; set
JAVA_HOMEin your shell profile. Homebrew installs export it themselves. - 404 from
/wheels/app/tests— the app isn't running asdevelopment(the allowlist above), or your server isn't pointing at this app'spublic/. - Port already in use — a previous
wheels startor another server holds the port;lsof -i :<port>and stop it. - Playwright JARs missing — browser specs skip with
this.browserTestSkipped = trueinstead of failing, so the suite stays green. Runwheels browser setuponce, or see Browser Tests. - Populate SQL errors — "Table already exists" means a stale table from a previous run; add
DROP TABLE IF EXISTSfirst. "No such function: NOW" (or similar) means cross-engine SQL drift; useCURRENT_TIMESTAMP— it's ANSI SQL and works everywhere, whileNOW()fails on SQLite and SQL Server. - "1 error(s)" with a bundle path but no failing assertion — the runner caught a load-time or setup error in that spec file. The two common causes:
it()called directly insiderun()without an enclosingdescribe(), orbeforeAll()throwing. Check the spec file at the path shown in the summary.