[Testbox 6.4.0] How Do I Purge Wirebox Singletons and/or Mocked Properties in Testbox?

I’ve run into what appears to be unexpected singleton state persistence when mocking with TestBox, and I want to sanity-check my understanding of how the ColdBox virtual app and WireBox singletons behave between tests and requests.

I always assumed that each request (and each test run) reset the ColdBox virtual app and WireBox singletons. However, I’m seeing mocked singleton methods persist across separate test CFCs, and even across subsequent requests, unless the server is restarted.

Reproduction

I started with the Ortus module template and have my tests running in the “test-harness” Coldbox application.

I have two tests in different test CFCs that interfere with each other when run together. All tests extend coldbox.system.testing.BaseTestCase

Test 1 – ClientProxyTest.cfc

...
beforeEach( function() {
	variables.myApi = getInstance( "myApi@my-api-wrapper" );
	prepareMock( variables.myApi );
} );

it( "should detect non-gateway methods using isGateway() convention", function() {
	var mockResponse = {
		"statusCode": "200",
		"data": { "authenticated": "banana" }
	};

	variables.myApi.$( "whoami", mockResponse );

	var proxy = getInstance( 
		name = "ClientProxy@my-api-wrapper", 
		initArguments = {
			parentObject = variables.myApi,
			methodName = "whoami",
			methodArgs = {}
		} 
	);

	// Execute after WireBox injection
	var result = proxy.execute();

	expect( proxy.isImmediateMethodProxy() ).toBeTrue( "whoami is not a gateway, should execute immediately" );
	expect( result ).toBeInstanceOf( "GatewayResult" );
} );

Test 2 – ApiTest.cfc

...
beforeEach( function(){
	variables.myApi = getInstance( "myApi@my-api-wrapper" );
} );

it( "has whoami() method that delegates to echo gateway", function(){
	
	prepareMock( variables.myApi.echo() ).$( "whoami" ).$results( { data : { user : "test" } } );

	var result = variables.myApi.whoami();

	debug( result ); <--- { "statusCode": "200","data": { "authenticated": "banana" } } 

	expect( result.data.user ).toBe( "test" ); <-- FAILS
	expect( variables.myApi.echo().$once( "whoami" ) ).toBeTrue();
} );
  • Each test passes when run individually
  • When run together, ApiTest.cfc receives the mocked response from ClientProxyTest.cfc
  • Even more surprising:
  • If I comment out ClientProxyTest.cfc entirely
  • And re-run tests in a new request
  • The mocked "banana" value still persists when debugged
  • The only way to clear it is to restart the server

Things I Tried

  • this.unloadColdbox = true
  • reset() in beforeEach() and beforeAll()
  • Testbox 5 and 6

Lucee 6 Result:

undefined tag [cfchart]; Failed in D:.…\my-api-wrapper\test-harness\coldbox\system\cache\report\skins\default\CacheCharting.cfm:5

Lucee 5 Result:

The key [MODULE_NAME] does not exist in the request scope, only the following keys are available: [$testid, cb_requestcontext, cbtransientdicache, coldboxvirtualapp, testbox]."

This behavior was surprising enough that I wanted to confirm whether I’m misunderstanding the lifecycle, or if this may indicate a bug or missing teardown step. Thanks in advance for any guidance.

This workaround appears to fix the issue:

Update the beforeAll() method in the ApiTest.cfc (the second test)

function beforeAll() {
        request.coldBoxVirtualApp.restart();
        super.beforeAll();
    }

However, I still would have assumed that the virtual app would restart itself between test specs. I guess I was wrong.

Can you post your Application.cfc/bx for the test harness?
Also note that if you put WireBox in a non standard application key, then this will happen also.

@lmajano here is the Application.cfc from /test-harness/

/**
********************************************************************************
Copyright 2005-2007 ColdBox Framework by Luis Majano and Ortus Solutions, Corp
www.ortussolutions.com
********************************************************************************
*/
component{

	// UPDATE THE NAME OF THE MODULE IN TESTING BELOW
	request.MODULE_NAME = "my-api-wrapper";

	// Application properties
	this.name              = hash( getCurrentTemplatePath() );
	this.sessionManagement = true;
	this.sessionTimeout    = createTimeSpan(0,0,15,0);
    this.setClientCookies  = true;

    /**************************************
	LUCEE Specific Settings
	**************************************/
	// buffer the output of a tag/function body to output in case of a exception
	this.bufferOutput 					= true;
	// Activate Gzip Compression
	this.compression 					= false;
	// Turn on/off white space managemetn
	this.whiteSpaceManagement 			= "smart";
	// Turn on/off remote cfc content whitespace
	this.suppressRemoteComponentContent = false;

	// COLDBOX STATIC PROPERTY, DO NOT CHANGE UNLESS THIS IS NOT THE ROOT OF YOUR COLDBOX APP
	COLDBOX_APP_ROOT_PATH       = getDirectoryFromPath( getCurrentTemplatePath() );
	// The web server mapping to this application. Used for remote purposes or static purposes
	COLDBOX_APP_MAPPING         = "";
	// COLDBOX PROPERTIES
	COLDBOX_CONFIG_FILE 	    = "";
	// COLDBOX APPLICATION KEY OVERRIDE
	COLDBOX_APP_KEY 		    = "";

    // Mappings
	this.mappings[ "/root" ] = COLDBOX_APP_ROOT_PATH;

	// Map back to its root
	moduleRootPath 	= REReplaceNoCase( this.mappings[ "/root" ], "#request.MODULE_NAME#(\\|/)test-harness(\\|/)", "" );
	modulePath 		= REReplaceNoCase( this.mappings[ "/root" ], "test-harness(\\|/)", "" );

	// Module Root + Path Mappings
	this.mappings[ "/moduleroot" ] = moduleRootPath;
	this.mappings[ "/#request.MODULE_NAME#" ] = modulePath;

	// ORM definitions: ENABLE IF NEEDED
	//this.datasource = "coolblog";
	//this.ormEnabled = "true";
	/**
	this.ormSettings = {
		cfclocation = [ "models" ],
		logSQL = true,
		dbcreate = "update",
		secondarycacheenabled = false,
		cacheProvider = "ehcache",
		flushAtRequestEnd = false,
		eventhandling = true,
		eventHandler = "cborm.models.EventHandler",
		skipcfcWithError = true
	};
	**/

	// application start
	public boolean function onApplicationStart(){
		application.cbBootstrap = new coldbox.system.Bootstrap( COLDBOX_CONFIG_FILE, COLDBOX_APP_ROOT_PATH, COLDBOX_APP_KEY, COLDBOX_APP_MAPPING );
		application.cbBootstrap.loadColdbox();
		return true;
	}

	// request start
	public boolean function onRequestStart(String targetPage){

		if( url.keyExists( "fwreinit" ) ){
			if( server.keyExists( "lucee" ) ){
				pagePoolClear();
			}
			// ORM reload: ENABLE IF NEEDED
			// ormReload();
		}

		// Process ColdBox Request
		application.cbBootstrap.onRequestStart( arguments.targetPage );

		return true;
	}

	public void function onSessionStart(){
		application.cbBootStrap.onSessionStart();
	}

	public void function onSessionEnd( struct sessionScope, struct appScope ){
		arguments.appScope.cbBootStrap.onSessionEnd( argumentCollection=arguments );
	}

	public boolean function onMissingTemplate( template ){
		return application.cbBootstrap.onMissingTemplate( argumentCollection=arguments );
	}

}

And just in case, here’s the Application.cfc from /test-harness/tests/

/**
 * Copyright 2005-2007 ColdBox Framework by Luis Majano and Ortus Solutions, Corp
 * www.ortussolutions.com
 * ---
 */
component {

	// The name of the module used in cfmappings ,etc
	request.MODULE_NAME = "my-api-wrapper";
	// The directory name of the module on disk. Usually, it's the same as the module name
	request.MODULE_PATH = "my-api-wrapper";

	// APPLICATION CFC PROPERTIES
	this.name                 = "#request.MODULE_NAME# Testing Suite";
	this.sessionManagement    = true;
	this.sessionTimeout       = createTimespan( 0, 0, 15, 0 );
	this.applicationTimeout   = createTimespan( 0, 0, 15, 0 );
	this.setClientCookies     = true;
	// Turn on/off white space management
	this.whiteSpaceManagement = "smart";
	this.enableNullSupport    = shouldEnableFullNullSupport();

	// Create testing mapping
	this.mappings[ "/tests" ] = getDirectoryFromPath( getCurrentTemplatePath() );

	// The application root
	rootPath                 = reReplaceNoCase( this.mappings[ "/tests" ], "tests(\\|/)", "" );
	this.mappings[ "/root" ] = rootPath;

	// The module root path
	moduleRootPath = reReplaceNoCase(
		rootPath,
		"#request.MODULE_PATH#(\\|/)test-harness(\\|/)",
		""
	);
	this.mappings[ "/moduleroot" ]            = moduleRootPath;
	this.mappings[ "/#request.MODULE_NAME#" ] = moduleRootPath & "#request.MODULE_PATH#";

	// ORM Definitions
	/**
	this.datasource = "coolblog";
	this.ormEnabled = "true";
	this.ormSettings = {
		cfclocation = [ "/root/models" ],
		logSQL = true,
		dbcreate = "update",
		secondarycacheenabled = false,
		cacheProvider = "ehcache",
		flushAtRequestEnd = false,
		eventhandling = true,
		eventHandler = "cborm.models.EventHandler",
		skipcfcWithError = false
	};
	**/

	function onRequestStart( required targetPage ){
		// Set a high timeout for long running tests
		setting requestTimeout   ="9999";
		// New ColdBox Virtual Application Starter
		request.coldBoxVirtualApp= new coldbox.system.testing.VirtualApp( appMapping = "/root" );

		// If hitting the runner or specs, prep our virtual app
		if ( getBaseTemplatePath().replace( expandPath( "/tests" ), "" ).reFindNoCase( "(runner|specs)" ) ) {
			request.coldBoxVirtualApp.startup( true );
		}

		// ORM Reload for fresh results
		if ( structKeyExists( url, "fwreinit" ) ) {
			if ( structKeyExists( server, "lucee" ) ) {
				pagePoolClear();
			}
			// ormReload();
			request.coldBoxVirtualApp.restart();
		}

		return true;
	}

	public void function onRequestEnd( required targetPage ){
		if ( request.keyExists(  "coldBoxVirtualApp" ) ) {
            request.coldBoxVirtualApp.shutdown();
        }
	}

	private boolean function shouldEnableFullNullSupport(){
		var system = createObject( "java", "java.lang.System" );
		var value  = system.getEnv( "FULL_NULL" );
		return isNull( value ) ? false : !!value;
	}

}