๐Ÿ”ท Core

testbox-unit-xunit

Use this skill when writing xUnit-style tests in TestBox using test functions (testXxx()), setup/teardown lifecycle (beforeTests/afterTests/setup/teardown), the $assert assertion object, grouped assertions with $assert.all() / assertAll(), retrying flaky tests with the retries method or bundle annotation, attaching files with attach(), skipping a single test or an entire test class with the class-level skip annotation, engine detection helpers (isBoxLang/isLucee/isAdobe), or the Arrange-Act-Assert (AAA) pattern for unit testing services, models, and utilities in isolation.

$ npx skills add coldbox/skills/testbox/unit
$ coldbox ai skills install coldbox/skills/testbox/unit
๐Ÿ”— https://skills.boxlang.io/skills/raw/coldbox/skills/testbox~unit

xUnit / Unit Testing with TestBox

When to Use This Skill

  • Writing xUnit-style test bundles (functions prefixed with test)
  • Using $assert assertion methods (isTrue, isEqual, includes, throws, etc.)
  • Writing beforeTests() / afterTests() / setup() / teardown() lifecycle methods
  • Unit-testing CFC models, services, or utilities in isolation with mocked dependencies
  • Skipping a single test function or an entire test class
  • Grouping related assertions with $assert.all() so every failure is reported at once
  • Applying the Arrange-Act-Assert (AAA) pattern

Language Reference

ConceptBoxLang (.bx) preferredCFML (.cfc) compatible
Class declarationclass extends="testbox.system.BaseSpec" {}component extends="testbox.system.BaseSpec" {}
Test functionsfunction testXxx() {}function testXxx() output="false" {}
Scoped varvar x = ...var x = ...

Canonical xUnit Bundle Structure

class labels="unit" extends="testbox.system.BaseSpec" {

    /****** LIFECYCLE ******/

    // Runs ONCE before all test functions in this bundle
    function beforeTests() {
        variables.service = new models.CalculatorService()
    }

    // Runs ONCE after all test functions
    function afterTests() {
        structClear( variables )
    }

    // Runs before EACH test function
    function setup() {
        variables.mockLogger = createMock( "models.Logger" )
        variables.service.setLogger( mockLogger )
    }

    // Runs after EACH test function
    function teardown() {
        mockLogger.$reset()
    }

    /****** TEST METHODS ******/

    function testAddsTwoNumbers() {
        // Arrange
        var a = 5
        var b = 3

        // Act
        var result = service.add( a, b )

        // Assert
        $assert.isEqual( 8, result )
    }

    function testDivideThrowsOnZero() {
        $assert.throws(
            () => service.divide( 10, 0 ),
            "MathException"
        )
    }

    function testSkipped() skip {
        $assert.fail( "Should never run" )
    }

}

Lifecycle Method Reference

MethodWhen It RunsUse Case
beforeTests()Once before all test functionsInitialize shared objects, DB connections, JWT settings
afterTests()Once after all test functionsClose connections, delete temp files
setup()Before each test functionCreate fresh mocks, reset state, clear caches
teardown()After each test functionRoll back transactions, delete records, reset stubs
function beforeTests() {
    // One-time: load heavy collaborators
    variables.orm = getInstance( "ORMService@cborm" )
    structClear( request )
}

function setup() {
    // Per-test: always get a clean state
    variables.mockDAO = createEmptyMock( "models.UserDAO" )
    variables.sut = new models.UserService( mockDAO )
}

$assert Assertion Reference

Every test bundle receives $assert โ€” an instance of testbox.system.Assertion.

// Boolean
$assert.isTrue( myBool )
$assert.isFalse( myBool )

// Equality
$assert.isEqual( expected, actual )
$assert.isEqualWithCase( expected, actual )
$assert.isNotEqual( expected, actual )

// Null
$assert.null( actual )
$assert.notNull( actual )

// Emptiness
$assert.isEmpty( target )     // arrays, structs, strings, queries
$assert.isNotEmpty( target )

// Size
$assert.lengthOf( target, length )
$assert.notLengthOf( target, length )

// Key existence
$assert.key( target, key )
$assert.notKey( target, key )
$assert.deepKey( target, key )
$assert.notDeepKey( target, key )

// Inclusion
$assert.includes( target, needle )              // case-insensitive
$assert.includesWithCase( target, needle )
$assert.notIncludes( target, needle )
$assert.notIncludesWithCase( target, needle )

// Type
$assert.typeOf( type, actual )
$assert.notTypeOf( type, actual )
$assert.instanceOf( actual, typeName )
$assert.notInstanceOf( actual, typeName )

// Numeric comparison
$assert.isGT( actual, target )
$assert.isGTE( actual, target )
$assert.isLT( actual, target )
$assert.isLTE( actual, target )
$assert.between( actual, min, max )
$assert.closeTo( expected, actual, delta )

// String / regex
$assert.match( actual, regex )
$assert.matchWithCase( actual, regex )
$assert.notMatch( actual, regex )

// Exceptions
$assert.throws( target, [type], [regex] )
$assert.notThrows( target, [type], [regex] )

// Force failure
$assert.fail( [message] )

// Skip current test
$assert.skip( message, detail )

BoxLang Dynamic Assertion Methods

In BoxLang you can also invoke any assertion as a free function prefixed with assert:

assertIsTrue( myBool )
assertIsEqual( expected, actual )
assertBetween( actual, 1, 100 )
assertThrows( () => badCall(), "MyException" )

Arrange-Act-Assert (AAA) Pattern

function testUserCreation() {
    // ARRANGE
    var mockUserDAO = createEmptyMock( "models.UserDAO" )
    mockUserDAO.$( "save" ).$results( { id: 42, name: "Alice" } )
    var sut = new models.UserService( mockUserDAO )
    var data = { name: "Alice", email: "[email protected]" }

    // ACT
    var result = sut.createUser( data )

    // ASSERT
    $assert.isEqual( 42, result.id )
    $assert.isEqual( "Alice", result.name )
    $assert.isTrue( mockUserDAO.$once( "save" ) )
}

Mixing xUnit with Expectations (expect DSL)

You can freely mix $assert and expect() fluent matchers in the same bundle:

function testUserEmail() {
    var user = sut.findById( 1 )

    // xUnit style
    $assert.isNotEmpty( user )
    $assert.key( user, "email" )

    // BDD fluent style (also available in xUnit bundles)
    expect( user.email ).toMatch( ".+@.+" )
    expect( user.isActive ).toBeTrue()
}

Skipping Tests

// Skip via function attribute
function testSomething() skip {
    $assert.fail( "won't run" )
}

// Skip via argument
function testEngineSpecific() skip="#!server.keyExists( 'lucee' )#" {
    $assert.isTrue( luceeOnlyFeature() )
}

// Skip programmatically inline
function testConditional() {
    if ( !featureEnabled ) {
        $assert.skip( "Feature flag is off" )
    }
    $assert.isTrue( myFeature.isActive() )
}

Skipping an Entire Test Class

TestBox 7.1+. A skip annotation on the class declaration skips the whole bundle: no test function runs, beforeTests/afterTests do not fire, and the bundle is left out of dry-run discovery entirely.

// BoxLang
class extends="testbox.system.BaseSpec" skip="true" {
    function testNothingRuns() {
        $assert.fail( "never reached" )
    }
}
// CFML
component extends="testbox.system.BaseSpec" skip="true" {
    function testNothingRuns(){
        $assert.fail( "never reached" );
    }
}

The annotation also accepts a method name on the class, which TestBox invokes to decide:

/**
 * Skipped off BoxLang: this bundle exercises BoxLang-only Set behaviour.
 */
class extends="testbox.system.BaseSpec" skip="isBoxLangMissing" {

    function isBoxLangMissing() {
        return !isBoxLang()
    }

    function testSetBehaviour() {
        $assert.isASet( setOf( 1, 2, 3 ) )
    }

}
skip valueEffect
skip="true" (or a boolean expression)Always skip the bundle
skip="" (present, empty)Always skip the bundle
skip="methodName"Call methodName() on the class; skip when it returns true
annotation absentRun normally

BaseSpec supplies the engine predicates these guards usually need: isBoxLang(), isLucee(), isAdobe(), isWindows(), isLinux(), isMac().

TestBox 7.1 fix: isLucee() used to return true under BoxLang, since BoxLang's CFML compatibility layer also populates server.lucee. It now returns false on BoxLang โ€” prefer isLucee() over a hand-rolled structKeyExists( server, "lucee" ) check, which still misfires.


Grouped Assertions โ€” $assert.all()

TestBox 7.1+. A test function stops at its first failed assertion. When several assertions describe one outcome, $assert.all() runs every closure and reports all the failures in a single aggregated error instead:

function testUserShape() {
    // Arrange / Act
    var user = variables.service.create( { name: "Alice", email: "[email protected]" } )

    // Assert โ€” every failure reported at once
    $assert.all( [
        () => $assert.key( user, "id" ),
        () => $assert.isEqual( "Alice", user.name ),
        () => $assert.isEqual( "[email protected]", user.email ),
        () => $assert.typeOf( "date", user.createdAt )
    ], "User shape" )
}

assertAll( executables, [heading] ) is the same call under a shorter name. See the testbox-assertions skill for the full contract.


Retrying Flaky Tests and Attaching Files

TestBox 7.2+. A retries annotation on a test method (or on the bundle class) reruns a failing or erroring test up to N extra times. Each attempt reruns setup(), the test and teardown(); only the final attempt is recorded. Skipped tests are never retried.

class extends="testbox.system.BaseSpec" retries="1" {   // bundle default

    function testChargesACard() retries="3" {           // method annotation wins
        var receipt = variables.gateway.charge( 100 )
        attach( receipt.pdfPath, "file", "receipt.pdf" ) // attach( path, type, name )
        $assert.isEqual( "paid", receipt.status )
    }

}

Precedence: method retries > bundle retries annotation > runner --retries=N. bx-playwright assertion failures (Playwright.AssertionFailed) count as test failures, not errors. For browser tests see the testbox-browser-testing skill.


Custom Assertions

Register in beforeTests() to keep the shared $assert object clean:

function beforeTests() {
    addAssertions( {
        isValidEmail: function( actual ) {
            return ( reFindNoCase( "^[^@]+@[^@]+\.[^@]+$", actual ) > 0
                ? true
                : fail( "[#actual#] is not a valid email address" ) )
        },
        isUUID: function( actual ) {
            return ( isValid( "uuid", actual )
                ? true
                : fail( "[#actual#] is not a UUID" ) )
        }
    } )
}

function testEmailValidator() {
    $assert.isValidEmail( "[email protected]" )
    $assert.isValidEmail( "not-an-email" )  // will fail
}

For reusable assertion libraries, register a class path or instance:

function beforeTests() {
    addAssertions( "tests.helpers.CustomAssertions" )
    // or
    addAssertions( new tests.helpers.CustomAssertions() )
}

Key Differences: xUnit vs BDD

AspectxUnitBDD
Test declarationfunction testXxx()it( "...", () => {} )
Suite declarationClass-leveldescribe( "...", () => {} )
LifecyclebeforeTests/setup/teardown/afterTestsbeforeAll/beforeEach/afterEach/afterAll/aroundEach
Assertions$assert.isXxx()expect().toBeXxx()
Skip a testskip function attributexit(), skip() inline
Skip a whole classclass-level skip annotation (7.1+)class-level skip annotation (7.1+)
Group assertions$assert.all() (7.1+)assertAll() (7.1+)
NestingNot supportedUnlimited nested describe blocks
Data bindingNot supportedit( data={} )

CommandBox Scaffolding

# Create xUnit spec
coldbox create unit name=UserServiceTest open=true

# Scaffold with specific model binding
coldbox create unit name=UserServiceTest methods=testCreate,testUpdate,testDelete