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

Assert Like You Mean It, TestBox 7.1 Part 3: Data Navigator

Assert Like You Mean It, TestBox 7.1 Part 3: Data Navigator

Almost every application eventually tests a nested piece of data: an API response, a JSON config file, a serialized object, module metadata. The data is a struct that contains an array that contains structs that contain more structs, and the thing you actually care about is one value buried at the bottom.

This post has two halves. First we look at BoxLang's Data Navigator, the language feature that makes nested data pleasant to work with. Then we look at the six TestBox 7.1 expectations built on top of it. If you have never used dataNavigate(), start at the top. If you already know it, skip to The six expectations.

Luis Majano
Luis Majano
September 29, 2026
try.boxlang.io Now Runs Every BoxLang Version, With or Without CFML

try.boxlang.io Now Runs Every BoxLang Version, With or Without CFML

The BoxLang playground at try.boxlang.io just got a major upgrade. You can now choose any released version of BoxLang and toggle CFML Compat mode on or off, right from the editor toolbar. No installs, no Docker images, no servers. Open a browser, write code, hit Run.

And the best part? The whole thing is powered by BoxLang Serverless on AWS Lambda, built and deployed from a single repository. Keep reading, because if you have ever wondered what BoxLang can do in the cloud, this is your answer. ☁️

Luis Majano
Luis Majano
September 28, 2026