How can I mock an AsyncResponder and the resultant handler functions on a mocked object using FlexUnit 4 and mockolate

1.1k views Asked by At

I am attempting to write some unit tests for a class I am writing in Flex 4.5.1 using FlexUnit 4 and Mockolate for my testing and mocking framework respectively. I am using as3-signals for my custom events.

The functionality that I am writing and testing is a wrapper class (QueryQueue) around the QueryTask class within the ArcGIS API for Flex. This enables me to easily queue up multiple query tasks for execution. My wrapper, QueryQueue will dispatch a completed event when all the query responses have been processed.

The interface is very simple.

public interface IQueryQueue
{
    function get inProgress():Boolean;
    function get count():int;

    function get completed():ISignal;
    function get canceled():ISignal;

    function add(query:Query, url:String, token:Object = null):void; 
    function cancel():void;
    function execute():void;
}

Here is an example usage:

public function exampleUsage():void
{
    var queryQueue:IQueryQueue = new QueryQueue(new QueryTaskFactory());
    queryQueue.completed.add(onCompleted);
    queryQueue.canceled.add(onCanceled);

    var query1:Query = new Query();
    var query2:Query = new Query();
    // set query parameters

    queryQueue.add(query1, url1);
    queryQueue.add(query2, url2);

    queryQueue.execute();
}

public function onCompleted(sender:Object, event:QueryQueueCompletedEventArgs)
{
    // do stuff with the the processed results
}

public function onCanceled(sender:Object, event:QueryQueueCanceledEventArgs)
{
    // handle the canceled event
}

For my tests I am currently mocking the QueryTaskFactory and QueryTask objects. Simple tests such as ensuring that queries are added to the queue relatively straight forward.

[Test(Description="Tests adding valid QueryTasks to the QueryQueue.")]
public function addsQuerys():void
{
    var queryTaskFactory:QueryTaskFactory = nice(QueryTaskFactory);
    var queryQueue:IQueryQueue = new QueryQueue(queryTaskFactory);
    assertThat(queryQueue.inProgress, isFalse());
    assertThat(queryQueue.count, equalTo(0));

    var query1:Query = new Query();
    queryQueue.add(query1, "http://gisinc.com");
    assertThat(queryQueue.inProgress, isFalse());
    assertThat(queryQueue.count, equalTo(1));

    var query2:Query = new Query();
    queryQueue.add(query2, "http://gisinc.com");
    assertThat(queryQueue.inProgress, isFalse());
    assertThat(queryQueue.count, equalTo(2));

    var query3:Query = new Query();
    queryQueue.add(query3, "http://gisinc.com");
    assertThat(queryQueue.inProgress, isFalse());
    assertThat(queryQueue.count, equalTo(3));
}

However, I want to be able to test the execute method as well. This method should execute all the queries added to the queue. When all the query results have been processed the completed event is dispatched. The test should ensure that:

  1. execute is called on each query once and only once
  2. inProgress = true while the results have not been processed
  3. inProgress = false when the results have been processed
  4. completed is dispatched when the results have been processed
  5. canceled is never called (for valid queries)
  6. The processing done within the queue correctly processes and packages the query results

So far I can write tests for items 1 through 5 thanks in large part to the answer provided by weltraumpirat. My execute test now currently looks like this.

[Test(async, description="Tests that all queryies in the queue are executed and the completed signal is fired")]
public function executesAllQueriesInQueue():void
{
    // Setup test objects and mocks
    var query:Query = new Query();
    var mockedQueryTask:QueryTask = nice(QueryTask);
    var mockedQueryTaskFactory:QueryTaskFactory = nice(QueryTaskFactory);

    // Setup expectations
    expect(mockedQueryTaskFactory.createQueryTask("http://test.com")).returns(mockedQueryTask);
    expect(mockedQueryTask.execute(query, null)).once();

    // Setup handlers for expected and not expected signals (events)
    var queryQueue:IQueryQueue = new QueryQueue(mockedQueryTaskFactory);
    handleSignal(this, queryQueue.completed, verifyOnCompleted, 500, null);
    registerFailureSignal(this, queryQueue.canceled);

    // Do it
    queryQueue.add(query, "http://test.com");
    queryQueue.execute();

    // Test that things went according to plan
    assertThat(queryQueue.inProgress, isTrue());
    verify(mockedQueryTask);
    verify(mockedQueryTaskFactory);

    function verifyOnCompleted(event:SignalAsyncEvent, passThroughData:Object):void
    {
        assertThat(queryQueue.inProgress, isFalse());
    }
}

The QueryQueue.execute method looks like this.

public function execute():void
{
    _inProgress = true;

    for each(var queryObject:QueryObject in _queryTasks)
    {
        var queryTask:QueryTask = _queryTaskFactory.createQueryTask(queryObject.url);
        var asyncToken:AsyncToken = queryTask.execute(queryObject.query);

        var asyncResponder:AsyncResponder = new AsyncResponder(queryTaskResultHandler, queryTaskFaultHandler, queryObject.token);
        asyncToken.addResponder(asyncResponder);
    }
}

private function queryTaskResultHandler(result:Object, token:Object = null):void
{
    // For each result collect the data and stuff it into a result collection
    // to be sent via the completed signal when all querytask responses
    // have been processed.
}

private function queryTaskFaultHandler(error:FaultEvent, token:Object = null):void
{
    // For each error collect the error and stuff it into an error collection
    // to be sent via the completed signal when all querytask responses
    // have been processed.
}

For test #6 above what I want to be able to do is to test that the data that is returned in the queryTaskResultHandler and the queryTaskFaultHandler is properly processed.

That is, I do not dispatch a completed event until all the query responses have returned, including successful and failed result.

To test this process I think that I need to mock the data coming back in the result and fault handlers for each mocked query task.

So, how do I mock the data passed to a result handler created via an AsyncResponder using FlexUnit and mockolate.

1

There are 1 answers

5
weltraumpirat On

You can mock any object or interface with mockolate. In my experience, it is best to set up a rule and mock like this:

[Rule]
public var rule : MockolateRule = new MockolateRule();

[Mock]
public var task : QueryTask;

Notice that you must instantiate the rule, but not the mock object.

You can then specify your expectations:

[Test]
public function myTest () : void {
    mock( task ).method( "execute" ); // expects that the execute method be called
}

You can expect a bunch of things, such as parameters:

    var responder:AsyncResponder = new AsyncResponder(resultHandler, faultHandler);
    mock( task ).method( "execute" ).args( responder ); // expects a specific argument

Or make the object return specific values:

    mock( queue ).method( "execute" ).returns( myReturnValue ); // actually returns the value(!)

Sending events from the mock object is as simple as calling dispatchEvent on it - since you're mocking the original class, it inherits all of its features, including EventDispatcher.

Now for your special case, it would to my mind be best to mock the use of all three external dependencies: Query, QueryTask and AsyncResponder, since it is not their functionality you are testing, but that of your Queue.

Since you are creating these objects within your queue, that makes it hard to mock them. In fact, you shouldn't really create anything directly in any class, unless there are no external dependencies! Instead, pass in a factory (you might want to use a dependency injection framework) for each of the objects you must create - you can then mock that factory in your test case, and have it return mock objects as needed:

public class QueryFactory {
    public function createQuery (...args:*) : Query {
       var query:Query = new Query();
       (...) // use args array to configure query
       return query;
    }
}

public class AsyncResponderFactory {
    public function createResponder( resultHandler:Function, faultHandler:Function ) : AsyncResponder {
        return new AsyncResponder(resultHandler, faultHandler);
    }
}

public class QueryTaskFactory {
    public function createTask (url:String) : QueryTask {
       return new QueryTask(url);
    }
}

... in Queue:

(...)
public var queryFactory:QueryFactory;
public var responderFactory : AsyncResponderFactory;
public var taskFactory:QueryTaskFactory;
(...)
var query:Query = queryFactory.createQuery ( myArgs );
var responder:AsyncResponder = responderFactory.createResponder (resultHandler, faultHandler);
var task:QueryTask = taskFactory.createTask (url);
task.execute (query, responder);
(...)

...in your Test:

[Rule]
public var rule : MockolateRule = new MockolateRule();

[Mock]
public var queryFactory:QueryFactory;
public var query:Query; // no need to mock this - you are not calling any of its methods in Queue.
[Mock]
public var responderFactory:AsyncResponderFactory;
public var responder:AsyncResponder;
[Mock]
public var taskFactory:QueryTaskFactory;
[Mock]
public var task:QueryTask;

[Test]
public function myTest () : void {
    query = new Query();
    mock( queryFactory ).method( "createQuery ").args ( (...) ).returns( query ); // specify arguments instead of (...)!

    responder = new AsyncResponder ();
    mock( responderFactory ).method( "createResponder" ).args( isA(Function) , isA(Function) ).returns( responder ); // this will ensure that the handlers are really functions 
    queue.responderFactory = responderFactory;

    mock( task ).method( "execute" ).args( query, responder );
    mock( taskFactory ).method( "createTask" ).args( "http://myurl.com/" ).returns( task );
    queue.taskFactory = taskFactory; 

    queue.doStuff(); // execute whatever the queue should actually do
}

Note that you must declare all mocks as public, and all of the expectations must be added before passing the mock object to its host , otherwise, mockolate cannot configure the proxy objects correctly.