jump to navigation

Unit Testing Commands - Part 2 - Mocking Command Dependencies July 27, 2008

Posted by Allen Manning in : Cairngorm, Flex, Unit Testing , 4comments

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

?View Code ACTIONSCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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

?View Code ACTIONSCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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.

?View Code ACTIONSCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
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.

Unit Testing Commands - Part I - Managing Complexity July 19, 2008

Posted by Allen Manning in : Cairngorm, Flex, Unit Testing , 4comments

We model our business domain with hundreds of interconnected objects and complex behavior emerges.

As the complexity grows it becomes less feasible for us to know what is really going on when the system behaves in unexpected ways- there are too many relationships and rules to keep in our head at one time. Unit testing help us manage this complexity by protecting us from regressions, and encouraging us not to force too much responsibility onto our objects. Unit tests also maintain knowledge about the assumptions of the objects and their relationships so we don’t need keep it in our head.

In this two-part post, I’m going to focus on unit test writing for Cairngorm Commands, and some techniques I’ve started using for faking dependencies when unit testing.

Commands are Core to Cairngorm Development

I use the Cairngorm micro-architecture for much of my Flex application development. The Command pattern is central to how Cairngorm facilitates collaborations between objects when executing a particular unit of work. Commands collaborate regularly with the following other types of objects:

By their very nature they have many dependencies, so writing unit tests for Commands can be a challenging and labor intensive endeavor. It is very easy for these Commands to have too much Fan Out and become overly complex.

Managing Command Complexity is key to reducing Cairngorm Application Complexity

The interconnections between Commands and other objects make testing more difficult, but this is good work to do- it is worth the investment. Commands are at the nexus of much object collaboration, and they map so directly to the actual growing business logic (or illogic) of the system. If we can get in there and test them properly, we will arrive some of the most effective application coverage.

This isn’t to say that most of the application’s logic should live in the Command. I wouldn’t advocate for that. To the contrary, I think the Model is an ideal place to move most of our business logic. But by their very nature, Commands tie the whole system together by bridging the gap between MV & C.

On a side note, I don’t think that Commands should have any relationship with Views. I’m a strong advocate for Model-Driven Cairngorm as a good approach to separating concerns in the application. There should be no know-of relationships between Command and views if it can be avoided.

Effective Unit Testing of Commands is a tool to reducing Command Complexity

Aside from effective coverage, a secondary gain to unit testing Commands is in how they help us refactor our code to reduce complexity. It is easy to make them overly complex and writing unit tests encourages us toward simplification.

I think it is fairly easy to make Commands into Transaction Scripts. The Delegate has just given us some results, and we have access to the model so why not just update it directly?  Our job is done, right?

Problems arise as the business logic surrounding updating the business model evolves very often organically. Business logic in our Commands becomes complex very easily and can be very conditionally nested. What is happening here is that our Commands are being forced to do too much work– we are asking them to know too much about the model.

I see an amalgam here with what Martin Fowler discusses in Patterns of Enterprise Application Architecture:

Using a Domain Model, as opposed to Transaction Script, is the essence of the paradigm shift that object-oriented people talk about so much. Rather than one routine having all the logic for a user action, each object takes a part of the logic that’s relevant to it.

We want to move away from having Transaction Script-esque Commands, and start to do some proper Domain Modeling. We are Object Oriented developers after all, right?

Writing Transaction Script unit tests is much more labor intensive because I find that there tends to be more dependencies and contexts you need to fake. If we move toward a Domain Model we more evenly distribute responsibility away from the Command and into the Model and our unit testing because much easier.

Up Next, code examples

Now that we have set the stage for why we want to invest in strategies for unit testing our Commands, in my next post I will give concrete code example of some ways to do it including a nice AS3 Mock Object framework I’ve just started using.