Can a mock method use its arguments in a callback function?

Thanks to Drew Bourne, not only was this question answered for Mockolate users, but a few small features were added to make the process significantly easier. For the sake of posterity, we will explain the method that makes this possible, make sure you have the necessary requirements, and show you the new shortcut that Drew has implemented.

For this method to work, you will need the latest build of Mockolate & Hamcrest-AS3, both projects maintained by Drew Bourne.

*IMPORTANT: Until the nightly build of mockolate is updated, you will need to get Drew's latest committ from Github. You will have to build the swc file from the project, the easiest way is with the automated Ant Build. If you need more information on setting that up, check out Setting Up Apache Ant Task.

Users in the described situation usually start with some kind of situation that looks vaguely like the following.

[Test]
public function urlLoaderTest():void {
    var urlRequest:URLRequest = new URLRequest( "xml/usersAndPasswords.xml" );
    urlProcessManager = new URLProcessManager();

    var urlLoader:URLLoader = nice( URLLoader );
    mock( urlLoader )
        .method( "load" )
        .args( instanceOf( URLRequest ) )
        .calls( function(request:URLRequest):void 
                {
                    urlProcessManager.processURL( request );
                } );

    urlProcessManager.loader = urlLoader;
    urlProcessManager.addURL( urlRequest ); // Calls the loader.load() method mocked above
}

• Instantiates the urlProcessManager variable declared within the case. Normally, it would be best to do this in [Before] method.
• Declares and instantiates a URLRequest object to retrieve the xml/usersAndPasswords.xml file.
• Prepares and injects a ‘nice’ mock of the URLLoader class.

We are confronted with an issue when mocking the arguments for the URLLoader.load() method. We need to make sure this method is called with an argument that is an instance of URLRequest, we then need to pass that same argument into another function call. In this case, the functionl is called by the urlProcessManager instance.

Unfortunately, there is no way in the current setup to pass the urlRequest variable passed into the urlProcessManager.addURL() call to the followup or 'callback' function of the mock.

The following is how can successfully call a callback method with the arguments passed into the mock.

[Test]
public function urlLoaderTest():void {
    urlProcessManager = new URLProcessManager();
    var urlRequest:URLRequest = new URLRequest( "xml/usersAndPasswords.xml" );

    var urlLoader:URLLoader = nice( URLLoader );
    mock( urlLoader )
        .method( "load" )
        .args( instanceOf( URLRequest ) )
        .answers( new CallsWithInvocationAnswer( function(invocation:Invocation):void 
                        {
                            var request:URLRequest = invocation.arguments[0] as URLRequest;
                            urlProcessManager.processURL( request );
                        } ));

    urlProcessManager.loader = urlLoader;
    urlProcessManager.addURL( urlRequest ); // Calls the loader.load() method mocked above
}

In this case, using a CallWithInvocationAnswer, this calls with an invocation object that contains the name, arguments, and return value of the mocked method.

The good news is that because this is a common enough, Drew has added a shortcut for Mockolate users in this exact scenario.

callsWithInvocation:

[Test]
public function urlLoaderTest():void {
    urlProcessManager = new URLProcessManager();
    var urlRequest:URLRequest = new URLRequest( "xml/usersAndPasswords.xml" );

    var urlLoader:URLLoader = nice( URLLoader );
    mock( urlLoader )
        .method( "load" )
        .args( instanceOf( URLRequest ) )
        .callsWithInvocation(
                                function(invocation:Invocation):void 
                                {
                                    urlProcessManager.processURL( invocation.arguments[0] as URLRequest );
                                } );

    urlProcessManager.loader = urlLoader;
    urlProcessManager.addURL( urlRequest ); // Calls the loader.load() method mocked above
}

callsWithArguments:

[Test]
public function urlLoaderTest):void {
    urlProcessManager = new URLProcessManager();
    var urlRequest:URLRequest = new URLRequest( "xml/usersAndPasswords.xml" );

    var urlLoader:URLLoader = nice( URLLoader );
    mock( urlLoader )
    .method( "load" )
    .args( instanceOf( URLRequest ) )
    .callsWithArguments(
        function(request:URLRequest):void 
        {
            urlProcessManager.processURL( request );
        } );

    urlProcessManager.loader = urlLoader;
    urlProcessManager.addURL( urlRequest ); // Calls the loader.load() method mocked above
}

Both options can work in the given situation, callsWithInvocation merely provides more information usable in the callBack method.

It is good to note that cases when this is necessary are rather specific. Furthermore, there are many ways that this can be avoided through creating loosely coupled code. We recommend taking a look at Miško Hevery's Guide To Writing Testable Code. If you abide by the outlined practices, you should be able to avoid this situation 99/100 times.