*Part 4 of 5 in our series on TestBox 7.1's 47 new assertions. Read the release announcement.
A validation spec usually checks several fields at once: name is set, email looks right, total is positive, status is one of an allowed list. Written as separate it() assertions, TestBox stops at the first failure and you fix it, rerun, and find out about the second failure. Five fields, potentially five rounds of that.
TestBox 7.1 adds grouped assertions that run every check and report every failure together, plus collection modes that apply one matcher across a whole array or struct without a loop.
The API
| Method | What it does |
|---|---|
assertAll( closure ) / $assert.all( closure ) | runs every expect() inside the closure, collects every failure, reports them all at once instead of stopping at the first |
expectAll( collection ) | applies the chained matcher(s) to every element; fails on the first element that doesn't match, reporting which one |
expectAny( collection ) | passes if at least one element matches |
expectSome( collection ) | alias-style variant of expectAny, reads naturally for "some of these should" |
expectNone( collection ) | passes only if no element matches |
withContext( label ) | prefixes failure messages with a label, useful inside grouped or looped assertions so you know which check failed |
expectAll already existed in TestBox for basic "every element passes this" checks (see the [2, 4, 6, 8] even-number example in TestBox's own test suite). What's new in 7.1 is assertAll/$assert.all for grouped, collect-everything reporting, and the Any/Some/None siblings alongside it.
A realistic example: validating an order
Order.bx (the SUT)
/**
* A minimal order model with a validate() that returns a struct
* of field-level problems (empty struct = valid).
*/
class {
property name="customerName" type="string";
property name="email" type="string";
property name="items" type="array";
property name="status" type="string";
variables.allowedStatuses = [ "pending", "paid", "shipped", "cancelled" ];
function init(
string customerName = "",
string email = "",
array items = [],
string status = "pending"
){
variables.customerName = arguments.customerName;
variables.email = arguments.email;
variables.items = arguments.items;
variables.status = arguments.status;
return this;
}
function subtotal(){
var sum = 0;
for ( var item in variables.items ) {
sum += ( item.price * item.qty );
}
return sum;
}
function isValidStatus(){
return variables.allowedStatuses.contains( variables.status );
}
}
OrderSpec.bx (the spec)
/**
* Grouped Assertions and Collection Modes in action: order validation
*/
class extends="testbox.system.BaseSpec" {
function run() {
describe( "Order", () => {
describe( "a well-formed order", () => {
beforeEach( () => {
variables.order = new Order(
customerName = "Alice Smith",
email = "alice@example.com",
items = [
{ "sku" : "WIDGET-1", "price" : 9.99, "qty" : 2 },
{ "sku" : "GADGET-2", "price" : 14.50, "qty" : 1 }
],
status = "paid"
);
} );
it( "passes every field check in a single grouped assertion", () => {
assertAll( () => {
expect( order.getCustomerName() ).notToBeEmpty();
expect( order.getEmail() ).toInclude( "@" );
expect( order.subtotal() ).toBeGT( 0 );
expect( order.isValidStatus() ).toBeTrue();
} );
} );
it( "computes the correct subtotal", () => {
expect( order.subtotal() ).toBeCloseTo( expected = 34.48, delta = 0.01 );
} );
} );
describe( "a malformed order", () => {
beforeEach( () => {
variables.order = new Order(
customerName = "",
email = "not-an-email",
items = [],
status = "bogus-status"
);
} );
it( "surfaces every field failure at once instead of stopping at the first", () => {
expect( function(){
assertAll( () => {
expect( order.getCustomerName() ).notToBeEmpty();
expect( order.getEmail() ).toInclude( "@" );
expect( order.isValidStatus() ).toBeTrue();
} );
} ).toThrow();
} );
} );
describe( "a batch of items", () => {
beforeEach( () => {
variables.items = [
{ "sku" : "WIDGET-1", "price" : 9.99, "qty" : 2 },
{ "sku" : "GADGET-2", "price" : 14.50, "qty" : 1 },
{ "sku" : "GIZMO-3", "price" : 4.25, "qty" : 5 }
];
} );
it( "every item has a positive price, checked without a loop", () => {
expectAll( items ).toSatisfy( ( item ) -> item.price > 0 );
} );
it( "at least one item has a quantity greater than one", () => {
expectAny( items ).toSatisfy( ( item ) -> item.qty > 1 );
} );
it( "no item has a negative quantity", () => {
expectNone( items ).toSatisfy( ( item ) -> item.qty < 0 );
} );
} );
describe( "serialized user output", () => {
it( "never includes a password field across any user", () => {
var users = [
{ "name" : "Alice", "email" : "alice@example.com" },
{ "name" : "Bob", "email" : "bob@example.com" }
];
expectNone( users ).toSatisfy( ( u ) -> u.keyExists( "password" ) );
} );
} );
it( "labels a check inside a loop so failures are easy to trace", () => {
var statuses = [ "pending", "paid", "shipped" ];
for ( var s in statuses ) {
expect( [ "pending", "paid", "shipped", "cancelled" ] )
.withContext( "status '#s#' should be allowed" )
.toInclude( s );
}
} );
} );
}
}
Run it:
box testbox run runner=OrderSpec.bx
Works on BoxLang and CFML
Grouped assertions and collection modes aren't tied to a BoxLang-only type, they run the same way on Lucee, Adobe, and BoxLang. If your specs have grown a habit of "one giant it() block with five separate expects," assertAll gives you the reporting benefit of splitting them up without actually splitting them up.
Next up, Part 5 (the finale): the rest of the 47, new scalar matchers, exception matching, skip annotations, and the smaller fixes that came along for the ride.
box install testbox@7.1.0
Add Your Comment