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

Your API Returns 200. Did the Business Transaction Actually Work?

Your API Returns 200. Did the Business Transaction Actually Work?

The customer submits an order. The portal shows a confirmation. The API returns 200 OK, and the release checks pass.

The next morning, operations asks why the order never appeared in the approval queue. Support has a ticket. A developer has to trace the order through the database and figure out whether someone can safely submit it again.

For teams maintaining a ColdFusion or Lucee application, this is a frustrating kind of release failure: the team ...

Maria Jose Herrera
Maria Jose Herrera
November 11, 2026
BoxLang AI 3.7: Profiles, Embeddings Defaults and MCP Rate Limiting

BoxLang AI 3.7: Profiles, Embeddings Defaults and MCP Rate Limiting

Ortus Solutions announces the release of BoxLang AI 3.7. Name a provider, model, params and API key once, then pick it by name for a call, an agent, or a single agent run. Embeddings get their own defaults, MCP servers you build get rate limiting and trusted proxies, and a handful of important fixes land along the way.

Luis Majano
Luis Majano
November 05, 2026
BoxLang  1.19.0: See Everything Your Code Does

BoxLang 1.19.0: See Everything Your Code Does

Ortus Solutions today announces the release of BoxLang 1.19.0, the software productivity platform for building, modernizing and running applications, with developers and AI agents working together. This release is about observability: built-in code coverage and execution profiling, the missing post-invocation events for BIFs, and new query and transaction events, and it pairs with the new BxLens beta, the debug bar and ops console that exposes hotspots and bytecode profilers. It also adds your own global settings in boxlang.json, static method interception, raw binary HTTP streaming and a lot of hardening across JDBC, Java interop and CFML compatibility.

Luis Majano
Luis Majano
November 05, 2026