Testing
Testing
Wheels ships a single BDD test runner — WheelsTest — that covers every layer of your app, from a model’s validations up through a real Chromium driving your rendered pages. This page is the map. It tells you which kind of test goes where, what a spec looks like, and how to run the suite. The detail pages that follow walk each category deeply.
You’ll learn:
- Which test categories Wheels recognizes and what each one is for
- The shape of a WheelsTest spec —
describe,it,expect - What changed from the legacy RocketUnit runner and why you shouldn’t use it for new work
- How to run the suite locally, and which files drive test setup
- Where to go next when you’re ready to write a specific kind of test
Test categories
Section titled “Test categories”Every test in a Wheels app is a CFC under tests/specs/, grouped by what it exercises. The runner treats them identically — the split is organizational, for you and your CI.
| Category | File location | Typical scope |
|---|---|---|
| Model tests | tests/specs/models/ | One model — validations, callbacks, scopes, custom methods |
| Controller tests | tests/specs/controllers/ | One action — response status, redirects, filters, params |
| View & form tests | tests/specs/view/ | Rendered HTML assertions, form-helper output, data-auto-id selectors |
| Integration tests | tests/specs/functional/ | Multi-step user journeys across controllers and requests |
| Functional tests | tests/specs/functional/ | Single-feature end-to-end — middleware + route + filter + action + view |
| Browser tests | tests/specs/browser/ | Full UI flows driven by Playwright — JavaScript, Turbo, form submission |
Model and controller tests are the bulk of a healthy suite. Integration tests cover journeys that span multiple actions and users. Functional tests verify one feature end-to-end through the full request pipeline. Browser tests cover what only exists in the browser — JavaScript-driven UI, Turbo Frame updates, anything where the server’s response alone doesn’t tell you whether the feature works.
What a WheelsTest spec looks like
Section titled “What a WheelsTest spec looks like”Every spec is a CFC that extends wheels.WheelsTest. The run() method holds describe(...) blocks. Inside each describe, it(...) blocks assert specific behaviors. Assertions chain off expect(...):
component extends="wheels.WheelsTest" { function run() { describe("Post", () => { it("requires a title", () => { var post = model("Post").new(); expect(post.valid()).toBeFalse(); expect(post.errorsOn("title")).toBeArray(); }); }); }}Key points:
- Extend
wheels.WheelsTest— notwheels.Test. The latter is the legacy RocketUnit base and doesn’t havedescribe,it, orexpect. describeandittake arrow-function callbacks. CFML closures work fine here.- Common matchers:
toBe,toBeTrue,toBeFalse,toBeArray,toBeStruct,toBeQuery,toInclude,toContain,toHaveKey,toHaveLength. - Phantom matchers that look plausible but don’t ship:
toEqual,toBeTruthy,toBeFalsy. UsetoBe(which deep-compares structs and arrays viaisEqual),toBeTrue/toBeFalse, or the specific type matchers instead. - Specs can nest
describeblocks as deep as you want. Shared setup goes inbeforeEach/afterEachinside thedescribe.
The detail pages for each category (below) walk through fixtures, matchers, and the patterns specific to that layer.
Legacy note — RocketUnit is deprecated
Section titled “Legacy note — RocketUnit is deprecated”Wheels used to ship with a homegrown runner called RocketUnit. Specs were CFCs in tests/ with method names prefixed test_ and assertions via assert(condition, message). That runner still loads for backward compatibility, but every new test in Wheels 4.0 should use WheelsTest BDD syntax. The BDD matchers are more expressive, the failures are easier to read, and the lifecycle hooks (beforeAll, beforeEach) are the same shapes you’ll recognize from other languages’ BDD frameworks. If you’re maintaining an older Wheels app, leave the RocketUnit tests alone — they still work — but write new tests against wheels.WheelsTest.
The reference platform
Section titled “The reference platform”Wheels 4.0 core tests run against Lucee 7 + SQLite. That’s the baseline you should assume when reading the test docs. The full matrix — Lucee 6/7, Adobe ColdFusion 2023/2025, BoxLang, and every supported database (MySQL, PostgreSQL, SQL Server, SQLite, CockroachDB, Oracle) — runs on a weekly schedule (plus manual dispatch) in the framework repo, not on every PR. For your own app, pick one engine and one database for fast feedback, then let a scheduled matrix job handle the rest. Cross-engine nuances (Lucee vs. Adobe scope rules, closure semantics, reserved names) surface as asides in the detail pages where they matter.
Running tests
Section titled “Running tests”The CLI driver is wheels test. It expects a running server — the runner boots your app in test mode, executes the specs, and reports pass/fail.
wheels --versionCommon invocations:
wheels test— execute the entire suite.wheels test --filter=models— a single directory or keyword.wheels test --ci— accepted for forward-compatibility, but currently a no-op: failure exit codes are non-zero on every run, with or without the flag. Browser-spec gating is controlled by the server’s environment (WHEELS_CI/WHEELS_BROWSER_CI_ENABLE), not by this flag.
The runner also exposes an HTTP endpoint at /wheels/app/tests you can hit directly. A URL like /wheels/app/tests?format=json&directory=tests.specs.models runs just the model specs and returns JSON. (The framework’s own suite lives at /wheels/core/tests, where the directory prefix is wheels.tests.specs — e.g. directory=wheels.tests.specs.model. A directory value the endpoint doesn’t recognize is silently ignored and the full suite runs instead; see #3083.) This is useful for dev-loop iteration — keep the server up, change a spec, refresh the URL. See Running Tests Locally for the full invocation surface including Docker-based cross-engine runs.
Setup files that matter
Section titled “Setup files that matter”Four files drive your test setup. Knowing what each one does saves a lot of head-scratching when a test passes in isolation but fails in the suite.
| File | What it does |
|---|---|
tests/TestRunner.cfc | Sets up shared state. Runs before and after the whole suite. |
tests/populate.cfm | Seeds test data. Runs once per test run, not per spec. |
tests/_assets/models/ | Test-only models, often using table() to map to test tables. |
tests/specs/<category>/ | Where your actual specs live. |
populate.cfm typically drops and recreates the test schema — it’s idempotent by construction and runs by default on every runner-URL hit (pass ?populate=false to opt out). This is a deliberate departure from Rails/Laravel-style per-spec rollback: the framework default is transactionMode = "none", so writes persist across specs unless you explicitly wrap them in transaction { ... transaction action="rollback"; }. Fixtures & Test Data walks through the real lifecycle and the per-spec isolation pattern.
CI integration
Section titled “CI integration”The Wheels repo ships with GitHub Actions workflows that run the full engine × database matrix on a weekly schedule. Your app’s CI doesn’t need to be nearly that heavy. A single job that runs wheels test against one engine and one database is usually enough. See CI Integration for drop-in templates, cross-engine matrix patterns, browser-test gating, and soft-fail database handling.