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:
The RemoteCarriergetCarrierGateway() method so it always returns a mock gateway object
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:
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 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:
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.
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
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.