Blog

Luis Majano

September 30, 2026

Spread the word


Share your thoughts

*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

MethodWhat 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

Resources

Add Your Comment

Recent Entries

QR Codes Come to BoxLang Images

QR Codes Come to BoxLang Images

The bx-image module can now generate and read QR codes. Two new functions, a string member function, and two new bx:image actions. No extra dependencies for you to manage.

Luis Majano
Luis Majano
October 14, 2026
ACF2021 extended support ends on November 10. Is Your Application Ready?

ACF2021 extended support ends on November 10. Is Your Application Ready?

Adobe ColdFusion 2021 reaches the end of extended support on November 10, 2026. If your application still runs on this release, the time to decide what comes next is now.

Adobe ended core support for ColdFusion 2021 on November 10, 2025. The current extended support period is best effort and does not include security patches or hotfixes. When it ends on November 10 2026, organizations will no longer have that support option from Adobe for ColdFusion 2021.

What ...

Cristobal Escobar
Cristobal Escobar
October 13, 2026