jump to navigation

ActionScript wrapper for the Brightcove Media APIs October 13, 2009

Posted by Allen Manning in : Brightcove, Flex, Refactoring , 1 comment so far

Brightcove has made some big strides in opening up their online service to developers in the last few years.  In particular, the Brightcove Media API, a JSON-based read and write API for interacting with the Brightcove platform, is a powerful tool for online experience development.

Developers who want to assemble customized online experiences can access the media libraries created by publishers and define custom visual experiences with really no limit.  Even though this is JSON-based, access to the Media APIs is not limited to AJAX clients. ActionScript clients as well have the full wealth of the Brightcove Media API at their disposal.

Let’s start by looking at a simple example application which loads a set of videos from the Brightcove Media APIs and populates them in a datagrid.

Before:  Example Application - Loading a Datagrid with Brightcove Videos

As is available in the documentation, the below application provides a fairly straight-forward approach to loading videos:

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
<?xml version="1.0" encoding="utf-8"?>
<mx:Application 
        xmlns:mx="http://www.adobe.com/2006/mxml">
 
    <!--
        ********************************************************************************************
        SCRIPT
        ********************************************************************************************
    -->
    <mx:Script>
    <![CDATA[
        import com.adobe.serialization.json.JSON;
        import mx.collections.ArrayCollection;
        import mx.controls.Alert;
 
        /** The object used to load URLs */
        private var _loader:URLLoader;
 
        /** The path to the brightcove api services */
        private const BRIGHTCOVE_API_PATH:String = "http://api.brightcove.com/services/library";
 
        /** The publisher token to be used to make the requests */
        private var _token:String = "0Z2dtxTdJAxtbZ-d0U7Bhio2V1Rhr5Iafl5FFtDPY8E."
 
        /** The command string to find all videos*/
        private const FIND_ALL_COMMAND_STRING:String = "find_all_videos";
 
        /** 
         * The "click" event listener for the find_all_videos button - load the videos from 
         * Brightcove convert them for JSON to native AS Objects and populate it in a datagrid
         */  
        private function onButtonClick():void {
 
            var url:String = BRIGHTCOVE_API_PATH + 
                "?command=" + 
                FIND_ALL_COMMAND_STRING +
                "&token=" +
                _token;
 
            trace('url to request: ' + url);
            var request:URLRequest = new URLRequest(url);
 
            _loader = new URLLoader();
            _loader.addEventListener(IOErrorEvent.IO_ERROR, errorHandler);
            _loader.addEventListener(Event.COMPLETE, loaderCompleteHandler);
 
            try {
                _loader.load(request);
            }
            catch (error:SecurityError) {
                trace("A SecurityError has occurred.");
            }
 
        }
 
        /** "fault" event listener to the remote call to the Brightcove media APIs */ 
        private function errorHandler(event:Event):void {
 
           mx.controls.Alert.show(
              "An error occurred" + 
                  event.target.toString(),
              "An error occurred");
 
        }
 
        /** "result" event listener to the remote call to the Brightcove media APIs */ 
        private function loaderCompleteHandler(event:Event):void {
 
            //Get the returned JSON data string
            var response:String = event.target.data as String;
 
            //The list of returned videos is embedded in the "items" property
            //of the root JSON object, so we will decode to a container
            var container:Object = (JSON.decode(response) as Object);
 
            videosDataGrid.dataProvider = new ArrayCollection(container.items);
 
            //Convert the UNIX date into an AS3 Date
            for(var i:int = 0; i< videosDataGrid.dataProvider.length; i++) {
                var video:Object = videosDataGrid.dataProvider.getItemAt(i);
                var n:Number = video.publishedDate;
                video.publishedDate = new Date(n);
            }           
 
        }
 
    ]]>
    </mx:Script>
 
    <!--
    	********************************************************************************************
    	VIEW
    	********************************************************************************************
    -->
    <mx:Button id="loadButton"
        label="Load Vidoes"
        click="onButtonClick()"/>
 
    <mx:DataGrid id="videosDataGrid"
            width="100%" height="100%">
 
        <mx:columns>
            <mx:DataGridColumn 
                dataField="id" 
                headerText="ID"/>
            <mx:DataGridColumn 
                dataField="name" 
                headerText="Name"/>
            <mx:DataGridColumn 
                dataField="shortDescription" 
                headerText="Description"/>
            <mx:DataGridColumn 
                dataField="publishedDate" 
                headerText="Published Date"/>
        </mx:columns>
    </mx:DataGrid>
 
</mx:Application>

Room for Improvement

Although the above is a good start, there are many areas of improvement. 

In particular:

After the refactoring
The below code, is the example application after it has been refactored to use a Brightcove Media API wrapper.  It cleans up the client code and frees ActionScript developers to work at a higher, more spacious level of abstraction

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
<?xml version="1.0" encoding="utf-8"?>
<mx:Application 
        xmlns:mx="http://www.adobe.com/2006/mxml">
 
    <!--
        ********************************************************************************************
        SCRIPT
        ********************************************************************************************
    -->
    <mx:Script>
    <![CDATA[
        import com.allenmanning.brightaction.api.MediaRequestErrorType;
        import mx.controls.Alert;
        import mx.collections.ArrayCollection;
        import com.allenmanning.brightaction.api.MediaRequestType;
        import com.allenmanning.brightaction.api.MediaRequest;
 
        /** The publisher token to be used to make the requests */
        private var _token:String = "0Z2dtxTdJAxtbZ-d0U7Bhio2V1Rhr5Iafl5FFtDPY8E."
 
        /** 
         * The "click" event listener for the find_all_videos button - work with the MediaRequest
         * Object to construct a "load videos" request, and register event listeners for handling
         * the response.
         */  
        private function onButtonClick():void {
 
            var mediaRequest:MediaRequest = new MediaRequest(
                _token,
                MediaRequestType.FIND_ALL_VIDEOS);
 
            mediaRequest.addEventListener(
                MediaRequest.RESULT_NAME,
                function():void {
                    videosDataGrid.dataProvider = mediaRequest.resultData;
                }
            );
 
            mediaRequest.addEventListener(
                MediaRequest.FAULT_NAME,
                function():void {
 
                    //tyepsafe error checking, no knowledge of BC Media API Error codes here
                    if(mediaRequest.error.errorType() == MediaRequestErrorType.INVALID_TOKEN) {
                        Alert.show("This token is not valid");
                    } else {
                        Alert.show(mediaRequest.error.errorType().toString());
                    }
 
                }
            );
 
            mediaRequest.execute();
 
        }
 
   ]]>
    </mx:Script>
 
    <!--
    	********************************************************************************************
    	VIEW
    	********************************************************************************************
    -->
    <mx:Button id="loadButton"
        label="Load Vidoes"
        click="onButtonClick()"/>
 
    <mx:DataGrid id="videosDataGrid"
            width="100%" height="100%">
 
        <mx:columns>
            <mx:DataGridColumn 
                dataField="id" 
                headerText="ID"/>
            <mx:DataGridColumn 
                dataField="name" 
                headerText="Name"/>
            <mx:DataGridColumn 
                dataField="shortDescription" 
                headerText="Description"/>
            <mx:DataGridColumn 
                dataField="publishedDate" 
                headerText="Published Date"/>
        </mx:columns>
    </mx:DataGrid>
 
</mx:Application>

In this application we have pushed the concerns of the API interaction down into the MediaRequest Object. The View is only responsible with interacting with with this wrapper or Remote Proxy.

Let’s look at a couple techniques being used here:


Typesafe and simplified error handling

The below code snippet is from MediaRequest, the object is responsible for knowing how to interact with Brightcove and parse the results. Additionally, all errors, remote faults and also client-side security exceptions are treated as being the same. It makes the interaction with the client cleaner, because there is only one type of fault to listen for.

The Media Request Object, also interacts with a collaborator: MediaRequestError. This Error object is responsible for storing all errors, client-side or server-side. All of these errors and registered, documented, with the understandable MediaRequestErrorType.

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
	  	//------------------------------------------------------------------------------------------
		// Methods
		//------------------------------------------------------------------------------------------
	    /** 
	     * Execute the Brightcove Media API asynchronous request for the token based upon the given 
	     * Media Request type.  
	     * 
	     * If a result has been returned, populate this object with the result converted into native
	     * AS3 objects, not JSON, and dispatch an event notifing listeners of this object that the 
	     * request has been completed.
	     * 
	     * If a fault has been returned, store the fault in this object and dispatch a Fault event 
	     * notifying listeners that a fault has occured.
	     */
	    public function execute():void {
 
	        // initilize existing result data
	        _resultData = null;
            _error = null;
 
            var url:String = BRIGHTCOVE_API_PATH + 
                "?command=" + 
                _requestType.toString() +
                "&token=" +
                _readToken;
 
            var request:URLRequest = new URLRequest(url);
 
            _loader = new URLLoader();
            _loader.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
            _loader.addEventListener(Event.COMPLETE, loaderCompleteHandler);
 
            try {
                _loader.load(request);
            } catch (error:SecurityError) {
                handleErrors(
                    new MediaRequestError(
                        MediaRequestErrorType.FLASH_SECURITY_ERROR, 
                        error));
            }
 
	    }
 
        /** Construct an error object and dispatch a fault event */
        private function handleErrors(error:MediaRequestError):void {
 
            _error = error;
            dispatchEvent(new Event(MediaRequest.FAULT_NAME));
 
        }
 
        /** "fault" event listener to the remote call to the Brightcove media APIs */ 
        private function ioErrorHandler(event:Event):void {
 
            handleErrors(new MediaRequestError(MediaRequestErrorType.IO_ERROR, event));
 
        }
 
        /**
         * Return true if the result of the call should be considerd a fault, false otherwise
         * 
         * @param result    the media API result as an ActionScript object
         */
        private function checkAndSetFault(result:Object):void {
 
            if(MediaRequestError.hasError(result)) {
 
                handleErrors(MediaRequestError.createFromResponse(result));
 
            }
 
        }
 
        /** "result" event listener to the remote call to the Brightcove media APIs */ 
        private function loaderCompleteHandler(event:Event):void {
 
            //Get the returned JSON data string
            var response:String = event.target.data as String;
 
            //The list of returned videos is embedded in the "items" property
            //of the root JSON object, so we will decode to a container
 
            //TODO Make the JSON decoding pluggable
            var result:Object = (JSON.decode(response) as Object);
 
            checkAndSetFault(result);
 
            if(_error != null) {
                return;
            }
 
            _resultData = new ArrayCollection(result.items);
 
            //Convert the UNIX date into an AS3 Date
            for(var i:int = 0; i< _resultData.length; i++) {
 
                var video:Object = _resultData.getItemAt(i);
                var n:Number = video.publishedDate;
                video.publishedDate = new Date(n);
 
            }           
 
            dispatchEvent(new Event(MediaRequest.RESULT_NAME));
 
        }


Self Documenting and Typesafe Error Handling with an AS “Enumeration”

Rather than sprinkling error parsing and checking code throughout our Brightcove ActionScript applications, defining them in a typesafe enum is a cleaner and more centralized place to document these conditions and lets other objects work with how they should be handled.

Without real Enum support, we define a set of static constants which are themselves instances a of the MediaRequestErrorType. This object defines a fault name and also registers and associated Error Code (if applicable).

The getErrorForCode() method lets us return a unqiue ErrorType object for a given error code. By using this registration-based approach, we have avoided any long conditional or awkward switch statements. As new errors are discovered, they are simply registered with this object, in a single location.

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
package com.allenmanning.brightaction.api {
 
    /**
     * An enumeration modeling the different types of Media Requests Errors
     * 
     * @author amanning
     */
    public class MediaRequestErrorType {
 
        //------------------------------------------------------------------------------------------
        // Member variables
        //------------------------------------------------------------------------------------------
        // Invalid Token was passed to the Media APIs
        public static const INVALID_TOKEN:MediaRequestErrorType = 
            MediaRequestErrorType.createAndRegister("INVALID_TOKEN", 210);
 
        // Request Validation Errors
        public static const VALIDATION_ERROR:MediaRequestErrorType = 
            MediaRequestErrorType.createAndRegister("VALIDATION_ERROR", 301);
 
        // Flash Communication Security Error
        public static const FLASH_SECURITY_ERROR:MediaRequestErrorType = 
            MediaRequestErrorType.createAndRegister("FLASH_SECURITY_ERROR");
 
        // IO Error, a problem with issuing the HTTP Get request to the media API
        public static const IO_ERROR:MediaRequestErrorType = 
            MediaRequestErrorType.createAndRegister("IO_ERROR");
 
        // Unknown Error, either not mapped into this wrapper API, or not invalid construction on 
        // the server
        public static const UNKNOWN_ERROR:MediaRequestErrorType = 
            MediaRequestErrorType.createAndRegister("UNKNOWN_ERROR");
 
        /** The toString value for the enum */
        private var _value:String;
 
        /** Return a string representation of this type */
        public function toString():String {
 
            return _value;
 
        }
 
        /** The error code associated with the error, if any */
        private var _code:Number;
 
        public function code():Number {
 
            return _code;
 
        }
 
        /** A look-up table for all of the codes, this gets initialized in the class constructor */
        private static var _codeLookupTable:Object;
 
        //------------------------------------------------------------------------------------------
        // Constructors
        //------------------------------------------------------------------------------------------
        /**
         * Class constructor - initialize the string representation of this enum
         * 
         * @param value     The string representation of this enum
         * @param code      An associated error code, if any
         */ 
        function MediaRequestErrorType(value:String,code:Number=-1) {
 
            _value = value;
            _code = code;
 
        }
 
        //------------------------------------------------------------------------------------------
        // Methods
        //------------------------------------------------------------------------------------------
 
        /**
         * Given a value and a code, create an error type and register it with the error lookup 
         * table
         * 
         * @param value     The string representation of this enum
         * @param code      An associated error code, if any
         * @return          Registered Media Request Error type
         */
        public static function createAndRegister(
                value:String,
                code:Number=-1):MediaRequestErrorType {
 
            var errorType:MediaRequestErrorType = new MediaRequestErrorType(value,code);
 
            if(_codeLookupTable == null) {
                _codeLookupTable = new Object();
            }
 
            if(errorType.code() != -1 && !_codeLookupTable.hasOwnProperty(code)) {
 
                _codeLookupTable[code] = errorType;
 
            }
 
            return errorType;
 
        }
 
        /**
         * Given an error code, return the corresponding ErrorType object, if it exists, or null 
         * otherwise
         * 
         * @param code  the error code to look up
         * @return      the corresponding ErrorType object, if it exists, or null otherwise
         */
        public static function getErrorForCode(code:Number):MediaRequestErrorType {
 
            if(_codeLookupTable.hasOwnProperty(code)) {
 
                return _codeLookupTable[code];
 
            }
 
            return null;
 
        }
 
    }   
 
}

Where to go from here?
A working example of this can be downloaded here, along with a set of Flexunit tests supporting this wrapper.

This is just the beginning, there are many places to go from here:

Dependency Injection in Flex Applications - Part I - Spring ActionScript and Cairngorm December 18, 2008

Posted by Allen Manning in : Cairngorm, Flex, Refactoring , 9comments

After layering many new features into our applications we see complexity emerge, and as complexity emerges we need to find techniques to keep our objects clean and testable. Complexity, as we looked at earlier, is a major challenge for large applications. Object complexity is increased as object collaboration increases; the more collaboration, the more complexity. This is complexity not only in objects performing their work, doing what they were designed to do, but also in testing.

We looked to some approaches in Unit Testing to help keep Command complexity under control and give our applications more robust testing coverage. Now we are going to upgrade these approach with some discussion and refactoring based on Dependency Injection.

Dependency Injection in a Nutshell
Eric Feminella has a great post on this that is worth reading.  Dependency Injection basically describes separating configuration from implementation. Swapping in and out different object dependencies in different application contexts hasn’t been implemented very cleanly in traditional Object Orientated languages.  In light of this, frameworks like the Spring Framework have risen in popularity over the last few years.

Christophe Herreman and Spring ActionScript

Many thanks to Christophe Herreman for releasing “Spring ActionScript” (formerly known as Prana) a Spring-like IoC container for Actionscript 3.  The project has been out for a while and has already gone through a few iterations.  Christophe has updated the Cairngorm Application Blueprint (the Cairngorm Store) with a Spring ActionScript Enabled version.  We will draw upon these ideas in Spring ActionScript-enabling an example application from a previous post.

Injecting Command Dependencies

Rather than declaring our two Command dependencies (the Model and a Delegate) as internal fields that unit tests in the same package can override (inject) with their Mocks, we will let Spring ActionScript do that work for us.  This results in a cleaner implementation where the Commands are completely de-coupled from our form of injection.

Let’s take a look at the refactoring:

UpdatePreferencesCommand.as (Before)

?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 {
 
        }
 
    }
 
}

UpdatePreferencesCommand.as (After, using Spring ActionScript)

?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
package com.allenmanning.exampleflexapp.account.commands {
 
    import com.adobe.cairngorm.commands.Command;
    import com.adobe.cairngorm.control.CairngormEvent;
    import com.allenmanning.exampleflexapp.account.business.DelegateLocator;
    import com.allenmanning.exampleflexapp.account.business.IAccountDelegate;
    import com.allenmanning.exampleflexapp.account.event.UpdatePreferencesEvent;
    import com.allenmanning.exampleflexapp.account.model.IAccountModels;
    import com.allenmanning.exampleflexapp.account.model.UserPreferences;
    import com.allenmanning.exampleflexapp.error.Assert;
    import com.allenmanning.exampleflexapp.model.ModelLocator;
 
    import mx.rpc.IResponder;
    import mx.rpc.events.ResultEvent;
 
    /**
     * 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 {
 
        //------------------------------------------------------------------------------------------
        // 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 {
 
            Assert.isTrue(
                ModelLocator.getInstance().accountModels.userIsLoggedIn,
                "The user must be logged in");
 
            var event:UpdatePreferencesEvent = (cairngormEvent as UpdatePreferencesEvent);
 
            Assert.notNull(event.user, "The user must not be null");
            Assert.notNull(event.userPreferences, "The preferences must not be null");
 
            var delegate:IAccountDelegate = DelegateLocator.getInstance().accountDelegate;
 
            delegate.updatePreferences(event.user.id, event.userPreferences);
 
        }
 
        /**
         * Event listener 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 {
 
            var updatedPreferences:UserPreferences =
                ((data as ResultEvent).result as UserPreferences);
 
            Assert.notNull(updatedPreferences, "The preferences must not be null");
 
            ModelLocator.getInstance().accountModels.preferences = updatedPreferences;                    
 
        }
 
        /**
         * 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 {
            // TODO implement this
        }
 
    }
 
}

There are two major differences in the above refactorings.
_accountDelegate has been removed. We were using an internal variable to inject the delegate into this class before. We would inject a mock for our unit tests after the object has been constructed. We have replaced this with a new DelegateLocator.

In the Prana Store example, the delegates were injected onto the Model Locator. This tight coupling between the business model and the delegates is something we should move away from. Delegates and Models should have now knowledge of each other.

The second major difference that we have added a basic implementation for the onResult function- the results are being set into the model. This is another area that we are using dependency injection. Both the accountDelegate and the accountModel are being injected using Spring ActionScript. The command itself is gaining access to these dependencies through two Singleton Locator Objects.

Injecting The Dependencies
In our application-context.xml we wire up the relationships. In this case, we are using mocks for testing. The DelegateLocator has a mock account delegate injected into it. The same goes for the Account Model in the ModelLocator. In both cases we are injecting Mocks for testing purposes. For a deployment context, the application-context.xml would be different.

application-context.xml

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
<objects xmlns="http://www.pranaframework.org/objects"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://www.pranaframework.org/objects http://www.pranaframework.org/schema/objects/prana-objects-0.6.xsd">
 
	<!-- =================================================================== -->
	<!-- This application context defines a single test configuration, it can be expanded to include
	       a deployment configuration as well, please see comments below.
 
		 1. Testing: Mock Delegates
		 2. Deployment: Remote Object Delegates
 
		 To use a configuration, uncomment it and comment the one you don't
		 want to use. By default, the Mock Delegates configuration is used.
 
	-->
	<!-- =================================================================== -->
 
	<!-- =================================================================== -->
	<!-- 1. Testing: Mock Delegates -->
	<!-- =================================================================== -->
	<object
	       id="DelegateLocator" 
	       class="com.allenmanning.exampleflexapp.account.business.DelegateLocator" 
	       factory-method="getInstance">
 
        <property name="accountDelegate">
            <object class="com.allenmanning.exampleflexapp.account.business.MockAccountDelegate"/>
        </property>
 
 
	</object>
 
    <object
           id="ModelLocator" 
           class="com.allenmanning.exampleflexapp.model.ModelLocator" 
           factory-method="getInstance">
 
        <property name="accountModels">
            <object class="com.allenmanning.exampleflexapp.account.model.MockAccountModels"/>
        </property>
 
    </object>
 
	<!-- =================================================================== -->
	<!-- 2. Deployment: Remote Object Delegates -->
	<!-- =================================================================== -->
    <!--
    <object
           id="DelegateLocator" 
           class="com.allenmanning.exampleflexapp.account.business.DelegateLocator" 
           factory-method="getInstance">
 
            <property name="accountDelegate">
	            <object class="TODO create real account delegate"/>
	        </property>
    </object>
 
 
    <object
           id="ModelLocator" 
           class="com.allenmanning.exampleflexapp.model.ModelLocator" 
           factory-method="getInstance">
 
        <property name="accountModels">
            <object class="com.allenmanning.exampleflexapp.account.model.AccountModels"/>
        </property>
 
    </object>
    -->
</objects>

Testing with Mocks and Spring ActionScript Our unit test for this command, assumes that Mocks have been injected into our Command and take advantage of these to test the object behavior.

?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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
package com.allenmanning.exampleflexapp.account.commands {
 
    import com.allenmanning.exampleflexapp.account.business.DelegateLocator;
    import com.allenmanning.exampleflexapp.account.business.IAccountDelegate;
    import com.allenmanning.exampleflexapp.account.business.MockAccountDelegate;
    import com.allenmanning.exampleflexapp.account.event.UpdatePreferencesEvent;
    import com.allenmanning.exampleflexapp.account.model.IAccountModels;
    import com.allenmanning.exampleflexapp.account.model.MockAccountModels;
    import com.allenmanning.exampleflexapp.account.model.User;
    import com.allenmanning.exampleflexapp.account.model.UserPreferences;
    import com.allenmanning.exampleflexapp.error.AssertionError;
    import com.allenmanning.exampleflexapp.model.ModelLocator;
    import com.anywebcam.mock.Mock;
 
    import flexunit.framework.TestCase;
 
    import mx.rpc.events.ResultEvent;
 
    /**
     * 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.  We have
         * injected this mock into the Delegate locator.
         */
        private var _mockAccountDelegate:IAccountDelegate =
            DelegateLocator.getInstance().accountDelegate;
 
        //------------------------------------------------------------------------------------------
        // 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 as MockAccountDelegate).mock = new Mock();
 
            var accountModel:IAccountModels = ModelLocator.getInstance().accountModels;
            (accountModel as MockAccountModels).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 as MockAccountDelegate).mock.verify();
            (ModelLocator.getInstance().accountModels as MockAccountModels).mock.verify();
 
        }
 
        public function testUserNotLoggedIn():void {
 
            var accountModel:IAccountModels = ModelLocator.getInstance().accountModels;
            (accountModel as MockAccountModels).mock.property("userIsLoggedIn").returns(false);
 
            try {
 
                _commandToTest.execute(_fakeEvent);
 
            } catch (error:AssertionError) {
 
                // expected
                return;
 
            }
 
            fail('An Assertion Failed Error should have been thrown');
 
        }
 
        /**
         * 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 {
 
            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(_fakePreferences,null));            
 
        }
 
        public function testExecuteNullDataPreferences():void {
 
            invalidDataTester(new UpdatePreferencesEvent(null, _fakeUser));            
 
        }
 
        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 as MockAccountDelegate).mock.method(
                    "updatePreferences").once.withArgs(
                        Number,
                        UserPreferences);
 
            var accountModel:IAccountModels = ModelLocator.getInstance().accountModels;
            (accountModel as MockAccountModels).mock.property("userIsLoggedIn").returns(true);
 
            // 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);
 
        }
 
        public function testOnResult():void {
 
            var accountModel:IAccountModels = ModelLocator.getInstance().accountModels;
            (accountModel as MockAccountModels).mock.property(
                "preferences").withArgs(UserPreferences);
 
            var stubbedResultEvent:ResultEvent = new ResultEvent(
                "result",
                false,
                true,
                _fakePreferences);
            _commandToTest.result(stubbedResultEvent);
 
        }
 
    }
 
}

There is no injection done in the test itself. The test assumes mocks have been injected into Command and we can set up expectations for how the command should interact with these mocks in different contexts.

This Flex Project includes all of the sample code run in a FlexUnit test suite.