Skip to content

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

wheels test detects your running dev server, reloads the app, hits the test runner, and prints pass/fail counts. No ceremony:

your shell — in your app root
# Run the full suite
wheels test
# Filter by directory (positional or --filter)
wheels test models
wheels test --filter=controllers
# Pick a reporter — simple (default), json, tap
wheels test --reporter=tap
# CI mode: emits GitHub Actions ::error annotations per failed/errored spec
wheels test --ci

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

The runner is a URL. With your app running in the development environment — under CommandBox, your own IIS/nginx setup, anything — hit:

browser or curl
# Run everything (browser-friendly HTML report)
http://localhost:8080/wheels/app/tests
# Machine-readable, and exactly what the CLI consumes
curl "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 as development (locally, or a CI job configured that way — see CI Integration).
  • The datasource is whatever the app is using. The <datasource>_test auto-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 have tests/populate.cfm rebuild — 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.

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

  • 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_HOME in your shell profile. Homebrew installs export it themselves.
  • 404 from /wheels/app/tests — the app isn't running as development (the allowlist above), or your server isn't pointing at this app's public/.
  • Port already in use — a previous wheels start or another server holds the port; lsof -i :<port> and stop it.
  • Playwright JARs missing — browser specs skip with this.browserTestSkipped = true instead of failing, so the suite stays green. Run wheels browser setup once, or see Browser Tests.
  • Populate SQL errors — "Table already exists" means a stale table from a previous run; add DROP TABLE IF EXISTS first. "No such function: NOW" (or similar) means cross-engine SQL drift; use CURRENT_TIMESTAMP — it's ANSI SQL and works everywhere, while NOW() 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 inside run() without an enclosing describe(), or beforeAll() throwing. Check the spec file at the path shown in the summary.