Skip to content

Commit 364a423

Browse files
committed
Phases 1 & 2: expectation context and collection expectation modes
Phase 1 - withContext(): - Add chainable method - Context prepended to all matcher failure messages, negated matchers, dynamic not* matchers, and custom matcher failures - resolveMessage() helper only injects when both context and message are non-empty to preserve Assertion default messages Phase 2 - Collection modes: - Add expectAny(), expectSome(), expectNone() to BaseSpec - Rewrite CollectionExpectation.cfc with mode dispatch - Detailed failure summaries with pass counts and element index/key - Use return-struct pattern for CFML pass-by-value safety - 22 new tests covering arrays, structs, edges, and chainability Verification: 381 passed, 0 failed, format check clean See plan.md for full roadmap
1 parent 19275eb commit 364a423

6 files changed

Lines changed: 684 additions & 55 deletions

File tree

changelog.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
## [Unreleased]
1111

12+
### Added
13+
14+
- Add expectation context support via `expect( value ).withContext( message )` that prepends semantic context to all failure messages including negated matchers and custom matchers.
15+
- Add collection expectation modes: `expectAny()`, `expectSome()`, and `expectNone()` alongside existing `expectAll()` with detailed failure summaries including element index/key and pass count reporting.
16+
17+
### Improvements
18+
19+
- Improve matcher failure messages with optional contextual prefix for distinguishing chained expectations.
20+
- Improve `expectAll()` failure messages to include pass/fail counts and per-element failure details with index/key context.
21+
22+
### Fixed
23+
24+
- Fix custom matcher failure messages not routing through the expectation's internal fail method.
25+
1226
## [7.0.0] - 2026-03-17
1327

1428
- <https://testbox.ortusbooks.com/readme/release-history/whats-new-with-7.0.0>

plan.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# TestBox Assertions And Expectations Roadmap
2+
3+
## Rules
4+
5+
- Every feature must include tests.
6+
- Full suite must pass before completion.
7+
- BoxLang-only features must skip cleanly on CFML engines.
8+
- Preserve backward compatibility for existing `$assert`, `expect()`, and `expectAll()` behavior.
9+
10+
## Verification
11+
12+
- [x] Run `box run-script format:check`
13+
- [x] Run `box testbox run`
14+
- [ ] Run BoxLang-specific tests for navigator, set, and range expectations
15+
- [ ] Confirm CFML engines skip BoxLang-only specs cleanly
16+
17+
## Phase 0: Test Harness Baseline
18+
19+
- [x] Identify existing assertion/expectation specs: `BaseAssertionsTest.cfc`, `AssertionsTest.cfc`, `BDDTest.cfc`
20+
- [ ] Add dedicated BoxLang-only specs for Data Navigators
21+
- [ ] Add dedicated BoxLang-only specs for Sets
22+
- [ ] Add dedicated BoxLang-only specs for Ranges
23+
24+
## Phase 1: Expectation Context (COMPLETE)
25+
26+
- [x] Add `Expectation.withContext( message )`
27+
- [x] Include context in matcher failure messages
28+
- [x] Include context in negated matcher failure messages
29+
- [x] Include context in custom matcher failures
30+
- [x] Add tests for passing and failing contextual expectations
31+
- [x] Inject context for dynamic `not*` matchers via `onMissingMethod`
32+
33+
## Phase 2: Collection Expectation Modes (COMPLETE)
34+
35+
- [x] Keep current `expectAll()` behavior unchanged
36+
- [x] Add `expectAny( collection )`
37+
- [x] Add `expectSome( collection, min = 1, max = 0 )`
38+
- [x] Add `expectNone( collection )`
39+
- [x] Support arrays
40+
- [x] Support structs
41+
- [x] Add pass count and index/key details to failure messages
42+
- [x] Add tests for all pass/fail combinations
43+
- [x] Add chainability tests for collection expectations
44+
45+
## Phase 3: Grouped Assertions
46+
47+
- [ ] Add `$assert.all( executables, heading = "" )`
48+
- [ ] Add `assertAll()` facade to `BaseSpec`
49+
- [ ] Add `expectAllAssertions()`
50+
- [ ] Add `TestBox.MultipleAssertionFailed` failure type
51+
- [ ] Aggregate assertion failures
52+
- [ ] Define behavior for unexpected exceptions
53+
- [ ] Add grouped assertion tests
54+
55+
## Phase 4: Low-Risk Matchers
56+
57+
- [ ] Add `$assert.isTruthy()`
58+
- [ ] Add `$assert.isFalsy()`
59+
- [ ] Add `toBeTruthy()`
60+
- [ ] Add `toBeFalsy()`
61+
- [ ] Add `toBeSameInstanceAs()`
62+
- [ ] Add `notToBeSameInstanceAs()`
63+
- [ ] Add `toHaveSize()`
64+
- [ ] Add `toThrowMatching( predicate )`
65+
- [ ] Add `toIncludeAll( needles )`
66+
- [ ] Add `toIncludeAny( needles )`
67+
- [ ] Add `toIncludeNone( needles )`
68+
- [ ] Add tests for all new matchers and negated forms
69+
70+
## Phase 5: Better Failure Diagnostics
71+
72+
- [ ] Add nested mismatch path reporting for arrays
73+
- [ ] Add nested mismatch path reporting for structs
74+
- [ ] Add query row/column mismatch reporting
75+
- [ ] Add lazy assertion messages
76+
- [ ] Improve `expectAll()` failure context
77+
- [ ] Add tests for diagnostics and lazy messages
78+
79+
## Phase 6: BoxLang Data Navigator Expectations
80+
81+
- [ ] Add runtime guard for BoxLang-only navigator features
82+
- [ ] Add `toHavePath( path )`
83+
- [ ] Add `toHavePathValue( path, expected )`
84+
- [ ] Add `toHavePathType( path, type )`
85+
- [ ] Add `toHavePathSatisfying( path, predicate )`
86+
- [ ] Add `path( path )` returning a normal `Expectation`
87+
- [ ] Add `queryPath( path )` returning an expectation over query results
88+
- [ ] Add tests for dot paths, indexes, wildcards, filters, recursive descent, and missing paths
89+
90+
## Phase 7: BoxLang Set Expectations
91+
92+
- [ ] Add `toBeSet()`
93+
- [ ] Add `toEqualSet( expected )`
94+
- [ ] Add `toBeSubsetOf( expected )`
95+
- [ ] Add `toBeSupersetOf( expected )`
96+
- [ ] Add `toBeDisjointFrom( expected )`
97+
- [ ] Add `toHaveUnion( other, expected )`
98+
- [ ] Add `toHaveIntersection( other, expected )`
99+
- [ ] Add `toHaveDifference( other, expected )`
100+
- [ ] Add `toHaveSymmetricDifference( other, expected )`
101+
- [ ] Add tests for default, linked, sorted, case-sensitive, Java Set, and numeric normalization behavior
102+
103+
## Phase 8: BoxLang Range Expectations
104+
105+
- [ ] Add `toBeRange()`
106+
- [ ] Add `toContainValue( value )`
107+
- [ ] Add `toContainRange( range )`
108+
- [ ] Add `toBeInRange( range )`
109+
- [ ] Add `toBeBeforeRange( range )`
110+
- [ ] Add `toBeAfterRange( range )`
111+
- [ ] Add `toBeBounded()`
112+
- [ ] Add `toBeUnbounded()`
113+
- [ ] Add `toBeHalfBounded()`
114+
- [ ] Add `toBeIterable()`
115+
- [ ] Add `toBeAscending()`
116+
- [ ] Add `toBeDescending()`
117+
- [ ] Add `toBeEmpty()`
118+
- [ ] Add `toHaveStep( step )`
119+
- [ ] Add `toClampTo( value, expected )`
120+
- [ ] Add tests for numeric, decimal, character, date, stepped, exclusive, unbounded, half-bounded, typed, and non-iterable ranges
121+
122+
## Phase 9: Data Navigator With Sets And Ranges
123+
124+
- [ ] Add `queryPath().asSet()` support
125+
- [ ] Support set expectations on navigator query results
126+
- [ ] Support range expectations on navigator path values
127+
- [ ] Add tests for path values in ranges
128+
- [ ] Add tests for query results converted to sets
129+
130+
## Phase 10: Partial And Asymmetric Matching
131+
132+
- [ ] Add `toContainSubset( sample )`
133+
- [ ] Add `toIncludeSubset( sample )`
134+
- [ ] Add `toMatchPattern( pattern )`
135+
- [ ] Add matcher helpers for any type, anything, string containing, string matching, array containing, struct containing, allOf, and anyOf
136+
- [ ] Add nested mismatch diagnostics
137+
- [ ] Add tests for partial and asymmetric matching
138+
139+
## Phase 11: Custom Equality And Object Formatters
140+
141+
- [ ] Add `addEqualityTester( closure )`
142+
- [ ] Add `addObjectFormatter( closure )`
143+
- [ ] Wire equality testers into deep equality
144+
- [ ] Wire object formatters into failure messages
145+
- [ ] Ensure test isolation for registered testers and formatters
146+
- [ ] Add tests for custom equality and custom formatting

system/BaseSpec.cfc

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -863,6 +863,59 @@ component {
863863
);
864864
}
865865

866+
/**
867+
* Start a collection expectation expression that passes when at least one element passes the chained matcher.
868+
* Returns an instance of CollectionExpectation in "any" mode.
869+
*
870+
* @actual The actual value, it should be an array or a struct.
871+
*/
872+
CollectionExpectation function expectAny( required any actual ){
873+
return new CollectionExpectation(
874+
spec = this,
875+
assertions = this.$assert,
876+
collection = arguments.actual,
877+
mode = "any"
878+
);
879+
}
880+
881+
/**
882+
* Start a collection expectation expression that passes when a bounded number of elements pass.
883+
* Returns an instance of CollectionExpectation in "some" mode.
884+
*
885+
* @actual The actual value, it should be an array or a struct.
886+
* @min The minimum number of elements that must pass, defaults to 1
887+
* @max The maximum number of elements that may pass, 0 means no upper bound
888+
*/
889+
CollectionExpectation function expectSome(
890+
required any actual,
891+
numeric min = 1,
892+
numeric max = 0
893+
){
894+
return new CollectionExpectation(
895+
spec = this,
896+
assertions = this.$assert,
897+
collection = arguments.actual,
898+
mode = "some",
899+
min = arguments.min,
900+
max = arguments.max
901+
);
902+
}
903+
904+
/**
905+
* Start a collection expectation expression that passes when zero elements pass the chained matcher.
906+
* Returns an instance of CollectionExpectation in "none" mode.
907+
*
908+
* @actual The actual value, it should be an array or a struct.
909+
*/
910+
CollectionExpectation function expectNone( required any actual ){
911+
return new CollectionExpectation(
912+
spec = this,
913+
assertions = this.$assert,
914+
collection = arguments.actual,
915+
mode = "none"
916+
);
917+
}
918+
866919
/**
867920
* Add custom matchers to your expectations
868921
*

0 commit comments

Comments
 (0)