Blog

Luis Majano

September 23, 2026

Spread the word


Share your thoughts

Part 1 of 5 in our series on TestBox 7.1's 47 new assertions. Read the release announcement.

If you have ever tested that two collections contain "the same stuff, order doesn't matter," you already know the workaround: sort both arrays, compare them element by element, and hope nobody adds a duplicate. It works, but it isn't what you meant to write. You meant "these are the same set."

BoxLang has a native Set type. TestBox 7.1 gives it nine matchers that speak the language of set theory directly, so your specs read like the requirement instead of a workaround for one.

The nine matchers

MatcherChecks
toBeASet()actual is a Set
toEqualSet( expected )same elements, order irrelevant
toBeSubsetOf( expected )every element of actual is in expected
toBeSupersetOf( expected )every element of expected is in actual
toBeDisjointFrom( expected )no elements in common
toHaveUnion( other, expected )actual ∪ other == expected
toHaveIntersection( other, expected )actual ∩ other == expected
toHaveDifference( other, expected )actual − other == expected
toHaveSymmetricDifference( other, expected )(actual − other) ∪ (other − actual) == expected

Every one of them has a not counterpart (notToBeASet(), notToEqualSet(), and so on), and every one accepts an optional custom message as the last argument.

// Old way: sort, then compare
expect( arraySort( duplicate( userRoles ), "text" ) )
    .toBe( arraySort( duplicate( expectedRoles ), "text" ) );

// New way: say what you mean
expect( setOf( userRoles ) ).toEqualSet( setOf( expectedRoles ) );

A realistic example: permission checks

Here's a small service that resolves a user's effective permissions from their assigned roles, and a spec that puts the set matchers through their paces. Both files are self contained. Drop them anywhere on your BoxLang class path and run the spec.

PermissionService.bx (the SUT)

/**
 * Resolves the effective permission Set for a user based on their roles.
 */
class {

	// Static role -> permission map for this example
	variables.rolePermissions = {
		"viewer"    : setOf( "read" ),
		"editor"    : setOf( "read", "write" ),
		"admin"     : setOf( "read", "write", "delete", "manageUsers" ),
		"billing"   : setOf( "read", "invoice", "refund" )
	};

	/**
	 * Returns the union of all permissions granted by the given roles.
	 *
	 * @roles An array of role names
	 */
	function getPermissionsForRoles( array roles ){
		var result = setOf();
		for ( var roleName in arguments.roles ) {
			if ( variables.rolePermissions.keyExists( roleName ) ) {
				result = result.union( variables.rolePermissions[ roleName ] );
			}
		}
		return result;
	}

	/**
	 * True if the user's permission set fully covers the required set.
	 *
	 * @userPermissions Set of permissions the user currently has
	 * @required Set of permissions the action requires
	 */
	function canPerform( set userPermissions, set required ){
		return arguments.required.isSubsetOf( arguments.userPermissions );
	}

}

PermissionServiceSpec.bx (the spec)

/**
 * Set Expectations in action: permission resolution
 */
class extends="testbox.system.BaseSpec" {

	function run() {
		describe( "PermissionService", () => {
			beforeEach( () => {
				variables.service = new PermissionService();
			} );

			it( "grants exactly the permissions of a single role", () => {
				var perms = service.getPermissionsForRoles( [ "editor" ] );

				expect( perms ).toBeASet();
				expect( perms ).toEqualSet( setOf( "read", "write" ) );
			} );

			it( "unions permissions across multiple roles", () => {
				var perms = service.getPermissionsForRoles( [ "editor", "billing" ] );

				expect( perms ).toHaveUnion(
					setOf( "read", "write" ),
					setOf( "read", "write", "invoice", "refund" )
				);
			} );

			it( "admin permissions are a superset of editor permissions", () => {
				var adminPerms  = service.getPermissionsForRoles( [ "admin" ] );
				var editorPerms = service.getPermissionsForRoles( [ "editor" ] );

				expect( adminPerms ).toBeSupersetOf( editorPerms );
			} );

			it( "viewer and billing roles do not overlap on write access", () => {
				var viewerPerms  = service.getPermissionsForRoles( [ "viewer" ] );
				var writeOnly    = setOf( "write", "delete", "manageUsers" );

				expect( viewerPerms ).toBeDisjointFrom( writeOnly );
			} );

			it( "denies an action when required permissions exceed the user's set", () => {
				var userPerms = service.getPermissionsForRoles( [ "viewer" ] );
				var required  = setOf( "read", "write" );

				expect( service.canPerform( userPerms, required ) ).toBeFalse();
				expect( required ).notToBeSubsetOf( userPerms );
			} );

			it( "allows an action when required permissions are fully covered", () => {
				var userPerms = service.getPermissionsForRoles( [ "admin" ] );
				var required  = setOf( "read", "delete" );

				expect( service.canPerform( userPerms, required ) ).toBeTrue();
				expect( required ).toBeSubsetOf( userPerms );
			} );

		} );
	}

}

Run it with the TestBox CLI:

box testbox run runner=PermissionServiceSpec.bx

BoxLang only, for now

The Set type and its literal helper setOf() are native to BoxLang. These nine matchers ship in TestBox 7.1 for every engine, but they only have something to grapple with on BoxLang. If you're still on Lucee or Adobe CFML, this is one more reason to try BoxLang, testing your set logic is meaningfully simpler once the language itself understands sets.


Next up, Part 2: 14 matchers for BoxLang's native Range type, bounds, steps, clamping, and the pagination logic you're probably still writing by hand.

box install testbox@7.1.0

Full release notes | TestBox docs | BoxLang

Add Your Comment

Recent Entries

ColdBox 8.2.0: Middleware, Streaming, AI Gateways, and a Whole Lot More

ColdBox 8.2.0: Middleware, Streaming, AI Gateways, and a Whole Lot More

ColdBox 8.2.0 is the result of work we started in March. Several of its headline features, including route-scoped middleware and Server-Sent Events, were incubated for months before shipping. We held them back on purpose: we wanted the APIs settled, the edge cases covered, and the testing story complete before asking you to build on them. They're ready now.

Luis Majano
Luis Majano
September 23, 2026
Is Your QA Process Ready for Your Next ColdFusion or BoxLang Release?

Is Your QA Process Ready for Your Next ColdFusion or BoxLang Release?

As applications continue to evolve, QA can easily become reactive. Teams may test late in the development process, focus only on happy-path scenarios, repeat manual checks, or rely on AI-generated changes without fully validating the results. ​ These gaps can create risks that are not always visible until they affect users, disrupt operations, or require costly fixes in production.

Maria Jose Herrera
Maria Jose Herrera
September 21, 2026
Your Development Team Is at Capacity. Here’s How to Keep Critical Work Moving!

Your Development Team Is at Capacity. Here’s How to Keep Critical Work Moving!

The problem is not that your team lacks priorities.It is that maintenance, delivery, and long-term improvement are competing for the same people.

For companies running business-critical CFML, ColdFusion, or BoxLang applications, that pressure can be especially difficult to solve. Experienced developers are not always easy to hire quickly, application knowledge may be concentrated in one or two people, and a generalist may need significant context before taking ownership of the work. A permanent hire can also require a significant investment in recruiting, compensation, onboarding, and long-term capacity even when the immediate need is a specific project, urgent maintenance, or a period of transition.

Maria Jose Herrera
Maria Jose Herrera
September 18, 2026