Using Stubs for HTTPService and RemoteObject in Flex
Brian LeGros | February 21st, 2009 | programmingUPDATE: I’ve finally created a project for these stubs on GitHub @ http://github.com/blegros/flexRpcStubs. Please consider this code more up to date then what follows below.
Recently I’ve been working on producing stub versions of HTTPService and RemoteObject for some integration tests I’m writing. If you haven’t worked with the concept of a stub before, think of a stub a re-implementation of an object created to produced canned answers to calls made to it during a test (borrowed from Mocks aren’t Stubs). A stub is intended to be purposefully ignorant to everything but what is being asked of it in a test. If you’re familiar with the concept of a mock object, a stub can be considered a specialization of a mock object that has no expectations to manage rather only return values. In a lot of circles the term mock and stub are used interchangeably, but I think the difference is important to note. If you’re interested more in the subject, come and see my presentation at FlexCamp Miami on March 6th, 2009.
Typically, in the case of integration testing, stubs are used to impersonate objects which have direct contact with resources external to your application, or component. In the context of most programming langauges I’ve worked with, these are typically classes built into extension libraries for the language (e.g. – JDBC, log4j, javax.mail, etc). In the context of Flex, services are the primary sets of classes that we end up wishing we could stub out. Unfortunately, the need to stub these types of classes can introduce some complexities, especially without a good mock object framework, because very rarely are the interfaces to these classes simple enough for a developer to impersonate on the fly (i.e. – inline in a single test). In the Java world, frameworks like Spring will create helper objects for testing to supplement these needs (e.g. – MockHttpSession and AbstractTransactionalJUnit4SpringContextTests).
In Flex, I haven’t really found a great set implementations for stubs yet, so this weekend I threw together a first draft of working stub classes for HTTPService and RemoteObject in AS3 using the Flex SDK 3.2. I’ve been using variants of these for my testing and so far, so good. Anyone interested in stubbing WebService, can proabably create a similar model to what I’ve done in RemoteObjectStub and be successful. Each stub is type-safe and adds the following properties for configuration to their base classes:
- delay : Number – Default: 1000 – Number of milliseconds to delay calling registered responders and dispatch the appropriate events for the stub.
- result : Function – The method used to map result, or fault, objects to specific signatures of send (for HTTPService) or a method name (for RemoteObject). If passed an object deriving from type
mx.rpc.Fault, amx.rpc.events.FaultEventwill be dispatched rather than amx.rpc.events.ResultEvent. - fault : Function – A sugar method that will take in a
faultCode,faultString, andfaultDetailto create amx.rpc.Faultand delegate toresult().
Each class also supports the dispatching of the appropriate events to mx.rpc.AsyncToken objects as well as the stub instances themselves, just like their base classes. Here is a example of using HTTPServiceStub, in place of HTTPService, in a test case written using fluint:
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 | public class ClassWithHTTPServiceDependencyTest extends TestCase { private var _classBeingTested : ClassWithHTTPServiceDependency; private var _stub : HTTPServiceStub; override protected function setUp() : void { _classBeingTested = new ClassWithHTTPServiceDependency(); _stub = new HTTPServiceStub("http://thisisntarealdomain.com"); _stub.delay = 500; _classBeingTested.service = _stub; } public function testSendWithTokenResult() : void { var result : Function = function (event : DynamicEvent, passThroughData : Object) : void { assertEquals("GOAL!", event.payload); }; _stub.result(null, "GOAL!"); _classBeingTested.addEventListener("success", asyncHandler(result, 2000)); _classBeingTested.someMethodUsingHTTPService(); } } |
In the above example, the behavior being tested is that when a ResultEvent is dispatched to ClassWithHTTPServiceDependency, it dispatches its own mx.events.DynamicEvent with a reference to the result property of the ResultEvent in its payload property. Please note, the call to send() is being made inside of someMethodUsingHTTPService() but could really be made anywhere inside of the object, we don’t care since the stub can impersonate HTTPService.
The implementations for the HTTPServiceStub and RemoteObjectStub are something I hope to get feedback on and maybe get included in fluint or mock-as3, although I’m sure Drew can think of sexier implementations. Below is the source for anyone who is interested in giving them a go to see if they can help your testing process. I have a few unit tests for each as well, so if anyone’s interested, just let me know and I’ll post that code as well.
Happy coding and here’s to these not working and failing you miserably …
HTTPServiceStub.as
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 | package net.digitalprimates.fluint.stubs { import flash.events.TimerEvent; import flash.utils.Dictionary; import flash.utils.Timer; import mx.rpc.AsyncToken; import mx.rpc.Fault; import mx.rpc.IResponder; import mx.rpc.events.AbstractEvent; import mx.rpc.events.FaultEvent; import mx.rpc.events.ResultEvent; import mx.rpc.http.HTTPService; /** * Idea borrowed from Sonke Rohde @ http://soenkerohde.com/2008/10/conditional-compilation-to-mock-with-swiz/ **/ public class HTTPServiceStub extends HTTPService { private var _resultData : Dictionary; //default num of milliseconds to wait before dispatching events //don't put too low otherwise your token responders may not be registered public var delay : Number = 1000; private var token : AsyncToken; private var parameters : Object; public function HTTPServiceStub(rootURL : String = null, destination : String = null) { super(rootURL, destination); _resultData = new Dictionary(); } public function result(parameters : Object, data : *) : void { _resultData[parameters] = data; } public function fault(parameters : Object, code : String, string : String, detail : String) : void { var fault : Fault = new Fault(code, string, detail); this.result(parameters, fault); } override public function send(parameters : Object = null) : AsyncToken { return configureResponseTimer(parameters); } private function configureResponseTimer(parameters : Object) : AsyncToken { token = new AsyncToken(null); this.parameters = parameters; //use a time to give time for the caller to map responders to the asyncToken var timer : Timer = new Timer(this.delay, 1); timer.addEventListener(TimerEvent.TIMER_COMPLETE, handleTimer); timer.start(); return token; } private function handleTimer(event : TimerEvent) : void { //clean-up event.target.removeEventListener(TimerEvent.TIMER_COMPLETE, handleTimer); //loop over all responders to emulate a successful call being made for each(var responder : IResponder in token.responders) { var response : Function = isFaultCall(parameters) ? responder.fault : responder.result; response.apply(null, [generateEvent(parameters)]); } //dispatch event to service just in case token wasn't used dispatchEvent(generateEvent(parameters)); } private function isFaultCall(parameters : Object) : Boolean { return (_resultData[parameters] is Fault); } private function generateEvent(parameters : Object) : AbstractEvent { if(isFaultCall(parameters)) { return new FaultEvent(FaultEvent.FAULT, false, true, _resultData[parameters]); } else { return new ResultEvent(ResultEvent.RESULT, false, true, _resultData[parameters]); } } } } |
RemoteObjectStub.as
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 | package net.digitalprimates.fluint.stubs { import flash.utils.Dictionary; import mx.rpc.AbstractOperation; import mx.rpc.Fault; import mx.rpc.remoting.RemoteObject; public dynamic class RemoteObjectStub extends RemoteObject { private var _resultData : Dictionary; //default num of milliseconds to wait before dispatching events //don't put too low otherwise your token responders may not be registered public var delay : Number = 1000; public function RemoteObjectStub(destination : String = null) { super(destination); _resultData = new Dictionary(); } public function result(methodName : String, args : Array, data : *) : void { if(!methodName || methodName.length == 0) { throw new Error("Cannot use null or empty method names in RemoteObjectStub."); } if(!args) { args = []; } if(!_resultData[methodName]) { _resultData[methodName] = new Dictionary(); } _resultData[methodName][args.toString()] = data; } public function fault(methodName : String, args : Array, code : String, string : String, detail : String) : void { var fault : Fault = new Fault(code, string, detail); this.result(methodName, args, fault); } override public function getOperation(name : String) : AbstractOperation { return new OperationStub(this, name, _resultData[name]); } } } import mx.rpc.AsyncToken; import net.digitalprimates.fluint.stubs.RemoteObjectStub; import mx.messaging.config.ServerConfig; import mx.rpc.Fault; import flash.utils.Dictionary; import mx.rpc.remoting.Operation; import mx.rpc.remoting.RemoteObject; import flash.events.TimerEvent; import mx.rpc.events.AbstractEvent; import mx.rpc.events.FaultEvent; import mx.rpc.events.ResultEvent; import flash.utils.Timer; import mx.rpc.IResponder; internal class OperationStub extends Operation { public var _resultData : Dictionary; private var token : AsyncToken; private var args : Array; public function OperationStub(remoteObject : RemoteObject, name : String, resultData : Dictionary) { super(remoteObject, name); _resultData = resultData; } override public function send(... args:Array) : AsyncToken { return configureResponseTimer(args); } private function configureResponseTimer(args : Array) : AsyncToken { token = new AsyncToken(null); this.args = args; var stub : RemoteObjectStub = RemoteObjectStub(service); //use a time to give time for the caller to map responders to the asyncToken var timer : Timer = new Timer(stub.delay, 1); timer.addEventListener(TimerEvent.TIMER_COMPLETE, handleTimer); timer.start(); return token; } private function handleTimer(event : TimerEvent) : void { //loop over all responders to emulate a successful call being made for each(var responder : IResponder in token.responders) { var response : Function = isFault(args) ? responder.fault : responder.result; response.apply(null, [generateEvent(args)]); } //send the result event to the RemoteObject as well service.dispatchEvent(generateEvent(args)); } private function isFault(args : Array) : Boolean { return (_resultData[args.toString()] is Fault); } private function generateEvent(args : Array) : AbstractEvent { if(isFault(args)) { return new FaultEvent(FaultEvent.FAULT, false, true, _resultData[args.toString()]); } else { var result : * = _resultData[args.toString()]; return new ResultEvent(ResultEvent.RESULT, false, true, _resultData[args.toString()]); } } } |
UPDATE: – Thanks to Joel Hooks for exercising these a bit more and figuring out a GC issue with the timer that provided some flukely behavior. The above code now supports his fixes in both HTTPServiceStub and RemoteObjectStub. I’ve also run my unit tests against the latest Flex 4 beta and everything passed. Looks like these may be more useful as we start to use Flex 4.

