Thursday, June 16, 2011

Darren on Flex: FlexUnit4 Testing with Parsley - Testing with a Parsley-Aware DSL

In Part-4 I showed how to use [RunWith] and [Rule] to locate and hide Parsley context initialization; with the reduced duplication in this approach tests are left to focus on what is important - testing.

In Part-5, I'll show how to reduce the noise hide the complexities of Parsley Messaging, used to implement decoupling between components, by extending the Fluint-aware DSL introduced in Part-3 to be Parsley-aware.

The 'test' code re-re-re-revisted...

Hiding Messages
As mentioned, this improved DSL attempts to hide uses of the Parsley Framework. The approach is limited to hiding usage of the Parsley Messaging API, but supports a comfortable balance of declarative and imperative coding seen in story-style testing i.e. given-when-then.

We retain the declarative approach to dependency injection i.e. with [Inject], yet this approach is less fitting for messaging/coupling management in a test(-method). Story tests tend to read as a sequence of imperative commands to prepare (Given), actuate (When) and inspect (Then) the object-under-test. In this style you often specify which message to dispatch and which to listen out for... and when... and for how long (timeout). You have the opportunity to state your messaging intentions clearly, in-line, that is, not having a test distributed over several methods (some of which being annotated with [MessageHandler(...)] metadata tags).

package com.darrenbishop.crm.directory.application {
    import com.darrenbishop.crm.directory.GetPersonCommandComponentTestContext;
    import com.darrenbishop.support.flexunit.parsley.ParsleyHelper;
    
    import org.flexunit.asserts.fail;
    import flash.events.ErrorEvent;
    import org.flexunit.assertThat;
    import org.hamcrest.object.equalTo;
    import com.darrenbishop.support.create;
    import com.darrenbishop.crm.directory.domain.Person;
    import com.darrenbishop.support.flexunit.parsley.ParsleyRule;
    
    public class GetPersonCommandParsleyRuleDSLComponentTest extends ParsleyHelper {
        [Rule]
        public var rule:ParsleyRule = new ParsleyRule(GetPersonCommandComponentTestContext);
        
        [Inject]
        public var service:StubDirectoryService;
        
        [Inject]
        public var command:GetPersonCommand;
        
        public var dodgyPerson:Person;
        
        public var soundPerson:Person;
        
        [Before]
        public function primeService():void {
            dodgyPerson = create(Person, {'id': 1, 'firstname': 'John001', 'lastname': 'Smith001', 'phone': 6803225});
            soundPerson = create(Person, {'id': 2, 'firstname': 'John002', 'lastname': 'Smith002', 'phone': 6809168});
            
            service.add(dodgyPerson, soundPerson);
            
            service.dodgyPerson = dodgyPerson.id;
            service.soundPerson = soundPerson.id;
        }
        
        [Test(async,description='Test GetPersonCommand result-handler does not mangle the person found.')]
        public function resultDoesNotManglePersonFound():void {
            dispatchMessage(PersonEvent.newLookup(soundPerson.id));
            waitForMessage(PersonEvent.FOUND, 1000);
            thenAssert(function(event:PersonEvent, data:*):void {
                assertThat(event.type, equalTo('PersonEvent.found'));
                assertThat(event.person.fullname, equalTo('John002 Smith002'));
            });
        }
        
        [Test(async,description='Test GetPersonCommand result-handler verifies the id of the person found.')]
        public function resultVerifiesPersonFound():void {
            dispatchMessage(PersonEvent.newLookup(dodgyPerson.id));
            waitForMessage(ErrorEvent.ERROR, 1000);
            thenAssert(function(event:ErrorEvent, data:*):void {
                assertThat(event.text, equalTo(sprintf('Found person (%d) does not match lookup person (%d)', soundPerson.id, dodgyPerson.id)));
            });
        }
    }
}
Note the test class now extends ParsleyHelper, building on the inheritance approach to provide access to the messaging-DSL methods; you'll see shortly that this new helper class extends from AsyncHelper, so the async-DSL methods are still available.

There are a number of changes to note and to make them clear I've provided a diff generated from the test above and that presented in Part-4:

--- C:/Dev/workspaces/flex/parsley-flexunit/src/test/flex/com/darrenbishop/crm/directory/application/GetPersonCommandParsleyRuleComponentTest.as Wed Jun 15 21:00:39 2011
+++ C:/Dev/workspaces/flex/parsley-flexunit/src/test/flex/com/darrenbishop/crm/directory/application/GetPersonCommandParsleyRuleDSLComponentTest.as Wed Jun 15 20:54:17 2011
@@ -4,3 +4,3 @@
     import com.darrenbishop.support.create;
-    import com.darrenbishop.support.flexunit.async.AsyncHelper;
+    import com.darrenbishop.support.flexunit.parsley.ParsleyHelper;
     import com.darrenbishop.support.flexunit.parsley.ParsleyRule;
@@ -8,3 +8,2 @@
     import flash.events.ErrorEvent;
-    import flash.events.EventDispatcher;
     
@@ -12,5 +11,4 @@
     import org.hamcrest.object.equalTo;
-    import org.spicefactory.parsley.core.messaging.MessageProcessor;
     
-    public class GetPersonCommandParsleyRuleComponentTest extends AsyncHelper {
+    public class GetPersonCommandParsleyRuleDSLComponentTest extends ParsleyHelper {
         [Rule]
@@ -18,5 +16,2 @@
         
-        [MessageDispatcher]
-        public var dispatcher:Function;
-        
         [Inject]
@@ -31,8 +26,4 @@
         
-        private var eventDispatcher:EventDispatcher;
-        
         [Before]
         public function primeService():void {
-            eventDispatcher = new EventDispatcher();
-            
             dodgyPerson = create(Person, {'id': 1, 'firstname': 'John001', 'lastname': 'Smith001', 'phone': 6803225});
@@ -46,16 +37,6 @@
         
-        [MessageError(type='com.darrenbishop.crm.directory.application.PersonEvent')]
-        public function rethrowIt(processor:MessageProcessor, error:Error):void { 
-            eventDispatcher.dispatchEvent(toEvent(error));
-        }
-        
-        [MessageHandler(selector='PersonEvent.found')]
-        public function passItOn(event:PersonEvent):void {
-            eventDispatcher.dispatchEvent(event);
-        }
-        
         [Test(async,description='Test GetPersonCommand result-handler does not mangle the person found.')]
         public function resultDoesNotManglePersonFound():void {
-            dispatcher(PersonEvent.newLookup(soundPerson.id));
-            waitFor(eventDispatcher, PersonEvent.FOUND, 1000);
+            dispatchMessage(PersonEvent.newLookup(soundPerson.id));
+            waitForMessage(PersonEvent.FOUND, 1000);
             thenAssert(function(event:PersonEvent, data:*):void {
@@ -68,4 +49,4 @@
         public function resultVerifiesPersonFound():void {
-            dispatcher(PersonEvent.newLookup(dodgyPerson.id));
-            waitFor(eventDispatcher, ErrorEvent.ERROR, 1000);
+            dispatchMessage(PersonEvent.newLookup(dodgyPerson.id));
+            waitForMessage(ErrorEvent.ERROR, 1000);
             thenAssert(function(event:ErrorEvent, data:*):void {
Counting the new lines (6) and the dropped lines (20, not including blank-lines) reveals a net 14 line reduction of boilerplate code that would otherwise be repeated as your tests grow in quantity and size.

With this improved DSL, tests become even simpler to read.

[MessageDispatcher] - refactored
As mentioned all the explicit Parsley Messaging Framework usage has been pulled up to the ParsleyHelper super class:
package com.darrenbishop.support.flexunit.parsley {
    import com.darrenbishop.support.flexunit.async.AsyncHelper;
    
    import flash.events.Event;
    import flash.events.EventDispatcher;
    
    import org.spicefactory.parsley.core.messaging.MessageProcessor;

    public class ParsleyHelper extends AsyncHelper {
        [MessageDispatcher]
        public var dispatcher:Function;
        
        protected var eventDispatcher:EventDispatcher;
        
        [Before]
        public function prepareEventDispatcher():void {
            eventDispatcher = new EventDispatcher();
        }
        
        [MessageError]
        public function rethrowIt(processor:MessageProcessor, error:Error):void { 
            eventDispatcher.dispatchEvent(toEvent(error));
        }
        
        [MessageHandler]
        public function passItOn(msg:Event):Boolean {
            return eventDispatcher.dispatchEvent(msg);
        }
        
        protected function dispatchEvent(event:Event):void {
            eventDispatcher.dispatchEvent(event);
        }
        
        protected function dispatchMessage(event:Event):void {
            dispatcher(event);
        }
        
        protected function dispatchError(error:Error):void {
            eventDispatcher.dispatchEvent(toEvent(error));
        }
        
        protected function waitForMessage(eventType:String, timeout:Number=1000):void {
            waitFor(eventDispatcher, eventType, timeout);
        }
        
        protected function waitForError(eventType:String, timeout:Number=1000):void {
            waitForMessage(eventType, timeout);
        }
    }
}
Here we have all the methods pertinent to Parsley messaging, laid out and self explanatory.

Furthermore this approach does a good job of hiding usage of the Flex Event mechanism; this messaging-DSL removes the need to interact explicitly with an EventDispatcher.

What's Next...

Part 6: All That, But With Mocking
So I've got one more Ace up my sleeve - mock-injection. At this point I'm not even sure this is a good idea or whether there's a place for this technique in any practical testing scenario. I have recently read the Spring documentation on this and have since seen this done in enough places to be convinced there is value in it for component/sub-system testing.

Nonetheless, I figured it out and implemented it; the implementation in fact requires some modifications, including some to Mockito-Flex classes. I've contributed these code changes to the Mockito-Flex project, which I believe will be rolled in or otherwise accommodated soon; you can read more about it here

4 comments:

Jérémy Banino said...

Hi Darren !
First, thanks for the articles, they are very helpful !
Second, I apologize for my poor English.

I use Parsley 3.0, and I have some problems with my context build.

I modify the buildContext method (BuildParsleyContext) like that :
var ctx:Context = FlexContextBuilder.build(contextDescriptor); ctx.addDynamicObject(test);

But I get an error: the command that I want to inject in my testClass is not known in the context, it seems that the tag [parsley:MapCommand type="{MyCommand}"/] is not interpreted in my context config file.
When I change the tag by [parsley:DynamicObject type="{MyCOmmand}"/], I can inject my command but i get timeout error in the waitForMessage function.

I try a lot of things like
FlexSupport.initialize();
var ctx:Context = ContextBuilder.newBuilder().config(FlexConfig.forClass(MyMXMLContext)).build();
MappedCommands.create(MyCommand).scope(ScopeName.LOCAL).register(ctx);

but it doesn't seem to work.

Have you got any idea about the problem ?
And could you share your context configuration file ?
Thanks in advance.
Jeremy

Darren Bishop said...

Hey, please note that I moderate comments to avoid spam and nobs.

I got your comment, the first time please relax your trigger finger :D

I've dusted off the code and will take a look as soon as I get the chance; I'll probably publish the whole thing to GitHub so you can see all the code.

Also note that at the time of writing P3 was not out and I have not updated the code to work. It is just a PoC

Jeremy Banino said...

Yep, sorry for the posts flow (First I thought that there was a proxy problem).I'll try to don't use autohotkey anymore to post comments :o
Otherwise, thanks for the rapid answer and thanks for publishing the whole thing on Git.
I continue to investigate on my side and if I find a workaround, I'll let you know.
Regards, Jeremy.

Darren Bishop said...

Right, I think it's your context configuration that is screwing you up; do not use ctx.addDynamicObject().

I vaguely remember having this problem before but I struggle to remember or reason why; I think it is to do with the fact that with this API, a child-context is created and the test is added there, but it's empty. Truth is I don't know/remember.

Yet having reproduced your problem on v2.2 by using that API try to stick with RuntimeConfigurationProcessor, which I believe is still available in v3.0; otherwise the API to use going forward seems to be in ContextBuilder:

var ctx:Context = ContextBuilder
.newBuilder()
.config(FlexConfig.forClass(MyMXMLContext))
.object(test, "test")
.build()

I've updated the gist on GitHub with the context configuration as you asked, but I don't think it will help as it is so trivial.

Anyway, post back and let me know how you get on. I'm quite chuffed that someone is still making use of this a whole year on. I must update it with the extra stuff I did that I haven't published yet... maybe weekend after next.