Skip to content

Commit 846f57b

Browse files
authored
Merge branch 'develop' into peter/query-builder-1908
2 parents ba56daf + 85a5dea commit 846f57b

File tree

15 files changed

+1088
-3
lines changed

15 files changed

+1088
-3
lines changed

.ai/wheels/testing/unit-testing.md

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# Unit & Integration Testing in Wheels
2+
3+
## Two Test Frameworks
4+
5+
Wheels has two test frameworks. **All new tests must use TestBox.**
6+
7+
| | TestBox (current) | RocketUnit (legacy) |
8+
|---|---|---|
9+
| **Syntax** | `describe`/`it`/`expect` (BDD) | `test_methodName()` + `assert()` |
10+
| **Base class** | `wheels.WheelsTest` | `wheels.tests.Test` |
11+
| **Location** | `tests/specs/` | `vendor/wheels/tests/` |
12+
| **Runner URL** | `/wheels/app/tests` | `/wheels/tests/core` |
13+
| **Status** | Active, all new tests | Legacy, backwards-compat only |
14+
15+
## TestBox Test Structure
16+
17+
### File Layout
18+
19+
```
20+
tests/
21+
_assets/
22+
models/ <- Test-only model CFCs (not app models)
23+
Model.cfc <- Base model (extends wheels.Model)
24+
Author.cfc <- Test model with table() override
25+
Post.cfc
26+
specs/
27+
models/ <- Model specs
28+
BatchProcessingSpec.cfc
29+
QueryBuilderSpec.cfc
30+
controllers/ <- Controller specs
31+
functional/ <- End-to-end specs
32+
populate.cfm <- Creates/seeds test tables (runs before every test suite)
33+
runner.cfm <- TestBox runner (web entry point)
34+
```
35+
36+
### Writing a Spec
37+
38+
```cfm
39+
component extends="wheels.WheelsTest" {
40+
function run() {
41+
describe("Feature Name", () => {
42+
43+
it("does something specific", () => {
44+
var result = model("author").findAll();
45+
expect(result.recordcount).toBeGT(0);
46+
});
47+
48+
it("returns a model object", () => {
49+
var obj = model("author").findOne(order="id");
50+
expect(IsObject(obj)).toBeTrue();
51+
expect(obj.firstName).toBe("Per");
52+
});
53+
54+
});
55+
}
56+
}
57+
```
58+
59+
### Key Points
60+
61+
- **Extend `wheels.WheelsTest`** — this injects all `application.wo` methods (like `model()`) into the test scope automatically.
62+
- **Use `function run()`** — TestBox calls this to discover specs. Not `init()`, not `config()`.
63+
- **Arrow functions work**`() => {}` is fine for `describe`/`it`/`beforeEach`.
64+
65+
## Test Models
66+
67+
Test models live in `tests/_assets/models/` and extend the local `Model.cfc` (which extends `wheels.Model`). They use `table()` to map to test tables created by `populate.cfm`.
68+
69+
```cfm
70+
// tests/_assets/models/Author.cfc
71+
component extends="Model" {
72+
function config() {
73+
table("c_o_r_e_authors");
74+
hasMany("posts");
75+
}
76+
}
77+
```
78+
79+
The test environment sets `modelPath` to `tests/_assets/models/` so `model("author")` resolves to your test model, not an app model.
80+
81+
## Test Data (populate.cfm)
82+
83+
`tests/populate.cfm` runs before every test suite invocation. It creates tables and seeds data.
84+
85+
**Always use DROP + CREATE, never IF NOT EXISTS:**
86+
87+
```cfm
88+
<!--- DROP first — IF NOT EXISTS misses schema changes --->
89+
<cftry>
90+
<cfquery datasource="#application.wheels.dataSourceName#">
91+
DROP TABLE IF EXISTS c_o_r_e_posts
92+
</cfquery>
93+
<cfcatch></cfcatch>
94+
</cftry>
95+
96+
<!--- Then CREATE --->
97+
<cfquery datasource="#application.wheels.dataSourceName#">
98+
CREATE TABLE c_o_r_e_posts (
99+
id #local.identityColumnType#,
100+
title varchar(250) NOT NULL,
101+
...
102+
PRIMARY KEY(id)
103+
) #local.storageEngine#
104+
</cfquery>
105+
```
106+
107+
**Why not IF NOT EXISTS?** If you add a column (like `status`) to a table that already exists from a previous test run, IF NOT EXISTS skips the CREATE and the column is missing. DROP + CREATE guarantees a clean schema every time.
108+
109+
## Running Tests
110+
111+
### Via URL (most reliable)
112+
113+
```
114+
# All specs in a directory
115+
/wheels/app/tests?format=json&directory=tests.specs.models
116+
117+
# Single spec bundle
118+
/wheels/app/tests?format=json&bundles=tests.specs.models.BatchProcessingSpec
119+
120+
# HTML output (for browser)
121+
/wheels/app/tests?format=html&directory=tests.specs.models
122+
123+
# Force model cache reload (needed after adding new model CFCs)
124+
/wheels/app/tests?format=json&directory=tests.specs.models&reload=true
125+
```
126+
127+
### Via curl + node (for CLI parsing)
128+
129+
```bash
130+
curl -sL "http://localhost:60006/wheels/app/tests?format=json&directory=tests.specs.models&reload=true" \
131+
> /tmp/testbox_results.json && node -e "
132+
const j = JSON.parse(require('fs').readFileSync('/tmp/testbox_results.json', 'utf8'));
133+
console.log('Passed:', j.totalPass, '| Failed:', j.totalFail, '| Errors:', j.totalError);
134+
for (const b of j.bundleStats) {
135+
console.log('\n' + b.name + ' (' + b.totalPass + '/' + b.totalSpecs + ')');
136+
function printSuite(s, indent) {
137+
for (const sp of (s.specStats || [])) {
138+
if (sp.status !== 'Passed') console.log(indent + '[FAIL] ' + sp.name + ': ' + sp.failMessage);
139+
}
140+
for (const ns of (s.suiteStats || [])) printSuite(ns, indent + ' ');
141+
}
142+
for (const s of b.suiteStats) printSuite(s, ' ');
143+
}
144+
"
145+
```
146+
147+
**Why node instead of jq?** The Wheels TestBox JSON response contains unquoted `true`/`false` booleans that break strict JSON parsers. Node's `JSON.parse` handles them.
148+
149+
## Common Gotchas
150+
151+
### 1. CFML Closure Scoping
152+
153+
Closures in CFML have their own `local` scope. You **cannot** read/write outer `local` variables from inside a closure.
154+
155+
```cfm
156+
// WRONG — `local.count` inside the closure is a DIFFERENT variable
157+
var count = 0;
158+
model("author").findEach(callback = function(author) {
159+
count++; // This modifies the closure's local.count, not the outer one
160+
});
161+
expect(count).toBe(10); // FAILS — count is still 0
162+
163+
// RIGHT — use a shared struct (structs are passed by reference)
164+
var result = {count: 0};
165+
model("author").findEach(callback = function(author) {
166+
result.count++; // Modifies the shared struct
167+
});
168+
expect(result.count).toBe(10); // PASSES
169+
```
170+
171+
### 2. Model Cache After Adding New CFCs
172+
173+
After adding a new model CFC to `tests/_assets/models/`, the first test run may fail with errors like `table 'authorscopeds' not found` — Wheels is using default table name conventions because it hasn't loaded your `config()` yet.
174+
175+
**Fix:** Add `&reload=true` to the test runner URL to clear the model cache.
176+
177+
### 3. Table Naming in Test Models
178+
179+
Always call `table()` in your test model's `config()` to map to the test table name. Without it, Wheels pluralizes the model name (e.g., `AuthorScoped` -> `authorscopeds`).
180+
181+
```cfm
182+
component extends="Model" {
183+
function config() {
184+
table("c_o_r_e_authors"); // Explicit table name
185+
}
186+
}
187+
```
188+
189+
### 4. Drop Order for Foreign Keys
190+
191+
Drop child tables before parent tables in `populate.cfm`:
192+
193+
```cfm
194+
DROP TABLE IF EXISTS c_o_r_e_posts <!--- child (has authorid FK) --->
195+
DROP TABLE IF EXISTS c_o_r_e_authors <!--- parent --->
196+
```
197+
198+
### 5. Pre-existing Test Failures
199+
200+
The `vendor/wheels/tests/` RocketUnit suite has some pre-existing failures (e.g., in `model.errors`). Don't chase these — they're known issues in the legacy suite. Focus on making your TestBox specs green.

.github/pull_request_template.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
## Summary
2+
3+
<!-- Brief description of what this PR does -->
4+
5+
## Related Issue
6+
7+
Closes #
8+
9+
## Type of Change
10+
11+
- [ ] Bug fix
12+
- [ ] New feature
13+
- [ ] Enhancement to existing feature
14+
- [ ] Documentation update
15+
- [ ] Refactoring
16+
17+
## Feature Completeness Checklist
18+
19+
<!-- All items must be checked for new features and enhancements -->
20+
21+
- [ ] **Tests** -- Unit tests covering happy path, edge cases, and error conditions
22+
- [ ] **Framework Docs** -- New or updated page in `docs/src/` with `SUMMARY.md` entry
23+
- [ ] **AI Reference Docs** -- New or updated file in `.ai/wheels/` directory
24+
- [ ] **CLAUDE.md** -- Updated if the feature changes model/controller/view conventions
25+
- [ ] **CHANGELOG.md** -- Entry under `[Unreleased]` section
26+
- [ ] **Test runner passes** -- All existing tests still pass (`/wheels/tests/core?format=json`)
27+
28+
## Test Plan
29+
30+
<!-- How to verify this PR works -->
31+
32+
## Screenshots / Output
33+
34+
<!-- If applicable -->

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ All historical references to "CFWheels" in this changelog have been preserved fo
2222

2323
### Added
2424
- Chainable query builder with `where()`, `orWhere()`, `whereNull()`, `whereBetween()`, `whereIn()`, `orderBy()`, `limit()`, and more for injection-safe fluent queries
25+
- Enum support with `enum()` for named property values, auto-generated `is*()` checkers, auto-scopes, and inclusion validation
26+
- Query scopes with `scope()` for reusable, composable query fragments in models
27+
- Batch processing with `findEach()` and `findInBatches()` for memory-efficient record iteration
2528

2629
----
2730

CLAUDE.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,11 +276,14 @@ Helpers: `linkTo(route="user", key=user.id, text="View")`, `urlFor(route="users"
276276

277277
## Testing Quick Reference
278278

279+
**All new tests use TestBox BDD syntax.** RocketUnit (`test_` prefix, `assert()`) is legacy only — never use it for new tests.
280+
279281
```cfm
282+
// tests/specs/models/MyFeatureSpec.cfc
280283
component extends="wheels.WheelsTest" {
281284
function run() {
282-
describe("User", function() {
283-
it("validates presence of name", function() {
285+
describe("My Feature", () => {
286+
it("validates presence of name", () => {
284287
var user = model("User").new();
285288
expect(user.valid()).toBeFalse();
286289
});
@@ -289,7 +292,13 @@ component extends="wheels.WheelsTest" {
289292
}
290293
```
291294

292-
Tests live in `tests/models/`, `tests/controllers/`, `tests/integration/`. Run with MCP `wheels_test()` or CLI `wheels test run`.
295+
- **Specs**: `tests/specs/models/`, `tests/specs/controllers/`, `tests/specs/functional/`
296+
- **Test models**: `tests/_assets/models/` (use `table()` to map to test tables)
297+
- **Test data**: `tests/populate.cfm` (DROP + CREATE tables, seed data)
298+
- **Runner URL**: `/wheels/app/tests?format=json&directory=tests.specs.models`
299+
- **Force reload**: append `&reload=true` after adding new model CFCs
300+
- **Closure gotcha**: CFML closures can't access outer `local` vars — use shared structs (`var result = {count: 0}`)
301+
- Run with MCP `wheels_test()` or CLI `wheels test run`
293302

294303
## Background Jobs Quick Reference
295304

@@ -356,6 +365,7 @@ Deeper documentation lives in `.ai/` — Claude will search it automatically whe
356365
- `.ai/wheels/controllers/` — filters, rendering, security
357366
- `.ai/wheels/views/` — layouts, partials, form helpers, link helpers
358367
- `.ai/wheels/database/` — migration column types, queries, advanced operations
368+
- `.ai/wheels/testing/` — unit testing with TestBox, test infrastructure, common gotchas
359369
- `.ai/wheels/configuration/` — routing, environments, settings
360370

361371
## MCP Server

CONTRIBUTING.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,19 @@ We welcome PRs of all sizes — from typo fixes to major features. To make revie
102102
* Use meaningful variable and function names
103103
* Add comments for complex logic
104104

105+
**Definition of Done:**
106+
107+
A feature or enhancement is not complete until all of the following are satisfied:
108+
109+
* **Tests** -- Unit tests covering happy path, edge cases, and error conditions in `vendor/wheels/tests/`
110+
* **Framework Docs** -- New or updated page in `docs/src/` with a corresponding entry in `docs/src/SUMMARY.md`
111+
* **AI Reference Docs** -- New or updated file in `.ai/wheels/` so AI assistants have accurate context
112+
* **CLAUDE.md** -- Updated if the feature changes model, controller, or view conventions
113+
* **CHANGELOG.md** -- Entry under the `[Unreleased]` section
114+
* **Test runner passes** -- All existing tests still pass (`/wheels/tests/core?format=json`)
115+
116+
Bug-fix PRs require tests and a CHANGELOG entry at minimum. Documentation-only PRs are exempt from the test requirement.
117+
105118
If you're making a **breaking change** or working on **core functionality**, it's best to open an Issue first to discuss the approach.
106119

107120
**Fork-and-Pull Workflow:**

docs/src/SUMMARY.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,9 @@
178178
* [Dirty Records](database-interaction-through-models/dirty-records.md)
179179
* [Soft Delete](database-interaction-through-models/soft-delete.md)
180180
* [Query Builder](database-interaction-through-models/query-builder.md)
181+
* [Enums](database-interaction-through-models/enums.md)
182+
* [Query Scopes](database-interaction-through-models/query-scopes.md)
183+
* [Batch Processing](database-interaction-through-models/batch-processing.md)
181184
* [Automatic Time Stamps](database-interaction-through-models/automatic-time-stamps.md)
182185
* [Database Migrations](database-interaction-through-models/database-migrations/README.md)
183186
* [Migrations in Production](database-interaction-through-models/database-migrations/migrations-in-production.md)

0 commit comments

Comments
 (0)