[Testbox + Mockbox] How Can I Mock a Method That Receives Required Arguments?

I’ve got a RemoteCarrier entity that interacts with 3rd party websites. The RemoteCarrier entity uses a factory to return the appropriate 3rd party gateway object and then executes a submit() method on it.

The process looks like this (abbreviated code)

// models/RemoteCarrier.cfc
function submit( required App app ) {

    // instance up a new result, that will be populated by the gateway
	var appResult = getInstance( "AppResult" );
	
	// Get the gateway object and submit the app
	getCarrierGateway().submit( arguments.app, appResult );
	
	// return the result entity
	return appResult;

}

I’d like to mock two things in TestBox:

  1. The RemoteCarrier getCarrierGateway() method so it always returns a mock gateway object
  2. A new CarrierGateway object that I can use to mutate the appResult to make sure it’s working.

Here’s how my Unit test looks so far (note: I am extending from coldbox.system.testing.BaseTestCase because I’m using Quick entities. I’m not sure if that makes a difference or not.

// tests/specs/unit/RemoteCarrierTest.cfc
// assert: `model` was prepped in beforeEach()
it( "Can submit remote apps", function() {

	// prep the RemoteCarrier entity for mocking magic
	prepareMock( model );
	model.setName( "Testbox Carrier" );
	model.setCode( "TESTBOX" );
	
	// Create a new mock object to emulate a carrier gateway since we don't want to actually send data
	var mockRemoteCarrierGateway = createMock("root.models.remoteCarrier.gateways.BaseRemoteCarrierGateway");
	
	// Mock the gateway's submit method to tweak the result
	mockRemoteCarrierGateway.$(
		method = "submit",
		callback = function( required app, required appResult ) {
			
			// mutate the appResult
			appResult.setStatus( "I was modified by Testbox" );


		}
	);
	
	// make sure that when the Remote Carrier requests the gateway, it always gets the mock gateway
	model.$(
		method = "getRemoteCarrierGateway",
		callback = function() {
			return mockRemoteCarrierGateway;
		}
	);
	
	// Create a random app and fill it with dummy data
	var remoteApp = getInstance( "RemoteApp" ).fill( {...} );
	
	// submit the dummy app to the Remote carrier model
	var remoteResult = model.submit( remoteApp );
	
	debug( remoteResult.getMemento() );
	
} );

When I check the debug code, I see the RemoteResult memento, but it hasn’t been mutated by the mock submit() method.

Just to make sure my getRemoteCarrierGateway mock works, I dumped out the result in the real RemoteCarrier’s submit() method. Indeed it’s working:

image

I went through the Mockbox docs and didn’t see an example of how do to this (or I completely missed it). Does anyone know how I can properly mock a method in this type of situation?

Why do you need your mocked method to mutate the code? As you are unit testing your RemoteCarrier method then you only really care about is that the submit method is being passed the expected arguments.

You would have a separate test for the CarrierGateway which can test to see it the submit method mutates the appResult as you expect.

I’d just have a mocked CarrierGateway submit method and then use TestBox $spy to assert that the correct arguments are being passed in. $spy() | TestBox : Behavior Driven Development (BDD)

as for the code you’ve posted, I think you really want this.

model.$( "getRemoteCarrierGateway", mockRemoteCarrierGateway );

$callback is useful when you need to handle things passed on the passed argument values.

1 Like

Thank you for the tips @aliaspooryorik

I took a look at the $spy documentation, and I’m not sure if the docs are incomplete, or I am missing something

Question about the Docs:

  1. In the example test, there’s a reference to a variable called CUT. What is that? I don’t see that variable defined anywhere. Perhaps it’s a typo in the example?

Regardless, I updated my test using your suggestions:

// tests/specs/unit/RemoteCarrierTest.cfc
// assert: `model` was prepped in beforeEach()
it( "Can submit remote apps", function() {

	// prep the RemoteCarrier entity for mocking magic
	prepareMock( model );
	model.setName( "Testbox Carrier" );
	model.setCode( "TESTBOX" );
	
	// Create a new mock object to emulate a carrier gateway and spy on the `submit()` method:
	var mockRemoteCarrierGateway = createMock("root.models.remoteCarrier.gateways.BaseRemoteCarrierGateway")
		.$spy( "submit" );
	
	// Ensure the model will use the mocked gateway
	model.$( "getRemoteCarrierGateway",mockRemoteCarrierGateway );
	
	// Create a random app and fill it with dummy data
	var remoteApp = getInstance( "RemoteApp" ).fill( {...} );
	
	// submit the dummy app to the model
	var remoteResult = model.submit( remoteApp );
	
	// Was the submit method called?
	debug( mockRemoteCarrierGateway.$count( "submit" ) ); // Yes! I can see it was called
	
	// Confirm the right argument(s) were passed to submit();
    // https://testbox.ortusbooks.com/v5.x/mocking/mockbox/verification-methods/usdcalllog
	debug(  mockRemoteCarrierGateway.$callLog() ); // I see the passed arguments!
	
} );

The changes you suggested work! I also have unit tests created for each RemoteCarrierGateway entity in separate test files.

However, I still can’t shake the feeling that it would still be nice to have some type of callback on the mocked “submit” method to perform some additional dummy object manipulation, but maybe I’m pushing the boundaries of a unit test too far.

Thanks again for taking the time to respond. :slight_smile:

That’s great you have working tests! Also that’s a good idea to assert the times the method is called as you have added as a debug. Sometimes you have code that is conditional or in a loop so I like to do an assert it was called once only

  1. In the example test, there’s a reference to a variable called CUT. What is that? I don’t see that variable defined anywhere. Perhaps it’s a typo in the example?

CUT is a term for ‘Component Under Test’ you sometimes see ‘SUT’ also which is ‘Subject Under Test’. It has no special meaning and isn’t magically created for you, it’s just a variable naming convention you sometimes see that holds the component you are testing against. In your code you’re extending BaseTestCase so it’s analogous with model which BaseTestCase creates for you.

BTW: the docs are published using GitBook so you can suggest edits.

You can do that. I’d have to write to be sure of the syntax which is why I didn’t post it before and I didn’t think it was what you needed to do as you wouldn’t really be testing anything of use. Asserting that the collaborator is called is valuable, testing that a mocked collaborator mutates a variable is just adding a test of your mock.