Unit Testing Commands – Part II – Mocking Command Dependencies

In the previous post, I argued that investing time in Unit Testing Commands in Cairngorm applications is a good strategy for increasing high quality coverage and reducing application complexity. Now let’s explore an example of how this argument plays itself out in the code. Let’s take a look at an example Command that we want to Unit Test.

A User Preferences Example

Imagine a generic Flex application where users can update their application preferences. We start with a simple User object which represents the individual who interacts with the application.

A user can have multiple application preferences- for example, which screen should be open after logging in, or if particular columns should be visible in particular DataGrids. These preferences will be managed by the UserPreferences object. Since our application is new, it simply maintains the application preference of whether the help screen should be visible after logging in.

Following the standard Cairngorm flow of control, in the View, we will dispatch UpdatePreferencesEvent with an updated UserPreferences object and the the UpdatePreferencesCommand gets executed.

Our Command

The UpdatedPreferenecesCommand may have many dependencies, and that is exactly what makes testing complex. Lets just focus on one for now- the a Cairngorm Business Delegate. This delegate will broker the asynchronous callback from whatever service is actually persisting our Users and Accounts.

Let’s assume that someone else is working on this class, and it isn’t done yet so we can agree upon an interface, IAccountDelegate and develop against that. Below you can see a diagram of how all of these objects are collaborating with each other.

Update Preferences - UML Entity Relationship Diagram

Replacing the Delegate with a Mock Object Wrapper

In each of our tests, we have certain expectations on how our Command will or will not interact with the Delegate. If the User has invalid data, then we don’t want the delegate to be called at all. If all of the information is valid, then we want the command to call the correct methods on the delegate.

We can more easily configure these expectations by using a generic Mock Object Framework and inject the mock into our command. I was please to find that Drew Bourne created this framework for me to use.

To set up our test, we first need to create a Mock of the IAccountDelegate and then we need to inject it into the Command. Below you can see how I’ve set up the mock in a wrapper class that delegates all calls to the mock.

MockAccountDelegate.as

package com.allenmanning.exampleflexapp.account.business {
    import com.allenmanning.exampleflexapp.account.model.UserPreferences;
    import com.anywebcam.mock.Mock;

    /**
     * A Mock Account Delegate- used in testing.  This class wraps a configurable mock object.
     *
     * @author amanning
     */
    public class MockAccountDelegate implements IAccountDelegate {

        //------------------------------------------------------------------------------------------
        // Member variables
        //------------------------------------------------------------------------------------------
        /**
         * The Mock that this class wraps, updates its exepecations for unit testing how
         * this object is interacted with
         */
        private var _mock:Mock;

        public function get mock():Mock {
            return _mock;
        }

        public function set mock(mock:Mock):void {
            _mock = mock;
        }

        //------------------------------------------------------------------------------------------
        // Methods
        //------------------------------------------------------------------------------------------
        /**
         * Given an User Id, and some application Preferences, make the remote request and broker
         * the asynchronous callback
         *
         * @param id            the identifier of the user to update
         * @param preferences   the user preferences to update
         */
        public function updatePreferences(id:Number, preferences:UserPreferences):void {

            // delegate this, and all calls to the mock, it will throw an exception if the call was
            // not expected
            _mock.updatePreferences(id,preferences);

        }

    }

}

I use an internal variable in the Command to initialize the delegate, this allows us to override it later with our Mock. Thanks to a fellow Brightcovean Adam Brod for showing me this technique- I think it is much cleaner than overriding a getDelegate() factory function.

UpdatePreferencesCommand.as

package com.allenmanning.exampleflexapp.account.commands {

    import com.adobe.cairngorm.commands.Command;
    import com.adobe.cairngorm.control.CairngormEvent;
    import com.allenmanning.exampleflexapp.account.business.IAccountDelegate;
    import com.allenmanning.exampleflexapp.account.event.UpdatePreferencesEvent;

    import mx.rpc.IResponder;

    /**
     * Command that updates application UserPreferences for a particular User- collaborates with
     * the Account Delegate for persistence, and updates the local Account Model
     *
     * @author amanning
     */
    public class UpdatePreferencesCommand implements Command, IResponder {

        //------------------------------------------------------------------------------------------
        // Member variables
        //------------------------------------------------------------------------------------------
        /** The remote account management proxy */
        internal var _accountDelegate:IAccountDelegate;

        //------------------------------------------------------------------------------------------
        // Constructors
        //------------------------------------------------------------------------------------------
        /**
         * Class constructor, initialize collaborators
         */
        public function UpdatePreferencesCommand():void {

            //once the account delegate has been created we will initialize it it here
            // _accountDelegate = new AccountDelegate(this);

        } 

        //------------------------------------------------------------------------------------------
        // Methods
        //------------------------------------------------------------------------------------------
        /**
         * Execute this command - update the Preferences and wait for response
         *
         * @param cairngormEvent    the CairngormEvent which was dispatched to invoke this command
         */
        public function execute(cairngormEvent:CairngormEvent):void {

            var event:UpdatePreferencesEvent = (cairngormEvent as UpdatePreferencesEvent);

            // Consider using an Assert?
            if(event.user == null || event.userPreferences == null) {
                throw new Error("Invalid event data");
            }

            _accountDelegate.updatePreferences(event.user.id, event.userPreferences);

        }

        /**
         * Event listner for the successful result from the delegate- update the local model with
         * the results
         *
         * @param data   The data passed back from the server.  While <code>data</code> is typed as
         *               Object, it is often (but not always) an mx.rpc.events.ResultEvent.
         * @see          mx.rpc.IResponder#result
         */
        public function result(data:Object):void {

        }

        /**
         * Event listener for the error result from the delegate- log the error
         *
         * @param info   The application fault.  While <code>info</code> is typed as Object it is
         * often (but not always) an mx.rpc.events.FaultEvent.
         *
         */
        public function fault(info:Object):void {

        }

    }

}

Writing the Test

Now we have come full circle, and have the means to more easily inject a mock dependency into our Command. Below is a series of tests that does this.

package com.allenmanning.exampleflexapp.account.commands {

    import com.allenmanning.exampleflexapp.account.business.MockAccountDelegate;
    import com.allenmanning.exampleflexapp.account.event.UpdatePreferencesEvent;
    import com.allenmanning.exampleflexapp.account.model.User;
    import com.allenmanning.exampleflexapp.account.model.UserPreferences;
    import com.anywebcam.mock.Mock;

    import flexunit.framework.TestCase;
    import flexunit.framework.TestSuite;

    /**
     * Responsible for testing the Command that updates application UserPreferences for a
     * particular User
     *
     * @author   amanning
     */
     public class UpdatePreferencesCommandTest extends TestCase {

        //------------------------------------------------------------------------------------------
        // Member Variables
        //------------------------------------------------------------------------------------------
        /** The command that we are testing */
        private var _commandToTest:UpdatePreferencesCommand;

        /** A fake user, used in testing */
        private var _fakeUser:User = new User(123456, 'aFakeUser');

        /** Fake user preferences, used in testing */
        private var _fakePreferences:UserPreferences = new UserPreferences(false);            

        /** A fake event used in testing */
        private var _fakeEvent:UpdatePreferencesEvent =
            new UpdatePreferencesEvent(_fakePreferences, _fakeUser);

        /** A mock Account delegate, with configurable expectations - used in testing */
        private var _mockAccountDelegate:MockAccountDelegate = new MockAccountDelegate();

        //------------------------------------------------------------------------------------------
        // Constructors
        //------------------------------------------------------------------------------------------
        /**
         * Class Constructor
         *
         * @param methodName    the test to add to this test suite
         * @see                 flexunit.framework.TestCase
         */
        public function UpdatePreferencesCommandTest( methodName:String = null ) {

            super( methodName );

        }

        //------------------------------------------------------------------------------------------
        // Methods
        //------------------------------------------------------------------------------------------
        /**
         * Called before every test to create testing data, or reset any test-specific properties
         *
         * @see    flexunit.framework.TestCase#setUp
         */
        override public function setUp():void {

            _commandToTest = new UpdatePreferencesCommand();
            _mockAccountDelegate.mock = new Mock();

        }

        /**
         * Called before after test to clean up testing data
         *
         * @see    flexunit.framework.TestCase#tearDown
         */
        override public function tearDown():void {

            // if this mock was not interacted with, and we expected it to this should throw an
            // exception
            _mockAccountDelegate.mock.verify();

        }

        /**
         * Returns testing suite, which can be used by a FlexUnit test runner
         *
         * @return the testing suite for this class
         */
        public static function suite():TestSuite {

            return new TestSuite(UpdatePreferencesCommandTest);

        }

        public function testExecute():void {

            //Configure a mock delegate and assign expectations.  We are expecting the Delegate's
            //updatePreferences method to be called only once with the correct arguments
            _mockAccountDelegate.mock.method(
                "updatePreferences").once.withArgs(Number,UserPreferences);

            // inject this mock dependency into our Command, we aren't testing the dependency,
            // only that it gets called properly
            _commandToTest._accountDelegate = _mockAccountDelegate;

            // The mock will throw an exception, if it's "updatePreferences" method isn't called
            // exactly once with a Number and UserPreferences type arguments.
            _commandToTest.execute(_fakeEvent);

        }

        /**
         * Testing utility function for executing the command with invalid data, execute should
         * throw an exception
         *
         * @param invalidFakeEvent  a fake event with invalid data
         *
         */
        private function invalidDataTester(invalidFakeEvent:UpdatePreferencesEvent):void {

            // Inject this mock dependency into our Command.  We do not need to update the
            // expectations since our only expectation is that the mock will not be called at all.
            // If it does get called, the verify in our teardown will throw an exceptions.
            _commandToTest._accountDelegate = _mockAccountDelegate;

            try {
                _commandToTest.execute(invalidFakeEvent);
            } catch(e:Error) {

                if(e.message.indexOf("No Expectation set for updatePreferences with args") != -1) {

                    fail("updatePreferences should not be called with invalid data");

                } else {
                    // It was expected to fail
                    return;
                }

            }

            fail("An exception was expected, the event was not properly populated");

        }

        public function testExecuteNullDataBoth():void {

            invalidDataTester(new UpdatePreferencesEvent(null,null));            

        }

        public function testExecuteNullDataUser():void {

            invalidDataTester(new UpdatePreferencesEvent(this._fakePreferences,null));            

        }

        public function testExecuteNullDataPreferences():void {

            invalidDataTester(new UpdatePreferencesEvent(null, _fakeUser));            

        }

    }

}

Our MockAccountDelegate is its own class which can be used, in the same way, for tests of any other Commands that collaborate with it. With this technique, developing and maintaining our Command unit tests is much less labor intensive.

That being said, there still are costs for each dependency associated with our Commands. This is the complexity feedback-loop I mentioned in previous post. As we develop unit tests we must weigh the testing costs of each new dependency we introduce. These testing costs encourage us to develop less complex objects with less dependencies.

You can get a the example Flex Project here which contains these examples. To run the examples, you must add mock-as3, FlexUnit, and Cairngorm to the Library path under Project Preferences / Flex Build Path in FB.