Tuesday, April 19, 2011

Darren on Flex: FlexUnit4 Testing with Parsley - Hiding Fluint Sequences with a Flow-based DSL

In Part-2 I showed how to implement a component-test, with some of the setup delegated to Parsley to leverage IoC and DI.

In Part-3, I'll show how to hide the Fluint Sequence API, used for asynchronous testing, behind an embedded asynchronous-DSL. The DSL design presented only really serves to seed an idea; anyone can implement a DSL, using different verb-names for methods and statement construction.

The 'test' code re-revisted...

package com.darrenbishop.crm.directory.application {
    import com.darrenbishop.crm.directory.GetPersonCommandComponentTestContext;
    import com.darrenbishop.crm.directory.domain.Person;
    import com.darrenbishop.support.create;
    import com.darrenbishop.support.flexunit.async.AsyncHelper;
    
    import flash.events.ErrorEvent;
    import flash.events.EventDispatcher;
    
    import org.flexunit.assertThat;
    import org.hamcrest.object.equalTo;
    import org.spicefactory.parsley.core.context.Context;
    import org.spicefactory.parsley.core.messaging.MessageProcessor;
    import org.spicefactory.parsley.flex.FlexContextBuilder;
    
    public class GetPersonCommandDSLComponentTest extends AsyncHelper {
        [MessageDispatcher]
        public var dispatcher:Function;
        
        [Inject]
        public var context:Context;
        
        [Inject]
        public var service:StubDirectoryService;
        
        [Inject]
        public var command:GetPersonCommand;
        
        public var dodgyPerson:Person;
        
        public var soundPerson:Person;
        
        private var eventDispatcher:EventDispatcher;
        
        [Before]
        public function initializeContext():void {
            var ctx:Context = FlexContextBuilder.build(GetPersonCommandComponentTestContext);
            ctx.createDynamicContext().addObject(this);
            
            eventDispatcher = new EventDispatcher();
            
            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;
        }
        
        [After]
        public function destroyContext():void {
            context.destroy();
            eventDispatcher = null;
        }
        
        [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);
            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 {
            dispatcher(PersonEvent.newLookup(dodgyPerson.id));
            waitFor(eventDispatcher, 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 that the test class now extends AsyncHelper, that is, I use inheritance to provide access to the async-DSL methods. An alternative approach is that adopted by Mockito-Flex, where a global object is used as/to share state between a suite of global functions implementing the mocking-DSL. I believe this is a safe approach as the Flex execution model is 'single-threaded'... I might explore this approach and free-up inheritance for local uses.

Anyway, my main objective in doing this is to remove any explicit use of the Fluint Sequence API and provide an abstraction that is a bit easier to use and read. To make the refactorings and changes a little more clear, I've provided a diff below generated against the test above and that from Part-2

--- C:/Dev/workspaces/flex/parsley-flexunit/src/test/flex/com/darrenbishop/crm/directory/application/GetPersonCommandComponentTest.as Fri Jun 17 07:26:56 2011
+++ C:/Dev/workspaces/flex/parsley-flexunit/src/test/flex/com/darrenbishop/crm/directory/application/GetPersonCommandDSLComponentTest.as Fri Jun 17 07:23:37 2011
@@ -4,2 +4,3 @@
     import com.darrenbishop.support.create;
+    import com.darrenbishop.support.flexunit.async.AsyncHelper;
     
@@ -9,4 +10,2 @@
     import org.flexunit.assertThat;
-    import org.fluint.sequence.SequenceRunner;
-    import org.fluint.sequence.SequenceWaiter;
     import org.hamcrest.object.equalTo;
@@ -16,3 +15,3 @@
     
-    public class GetPersonCommandComponentTest {
+    public class GetPersonCommandDSLComponentTest extends AsyncHelper {
         [MessageDispatcher]
@@ -35,5 +34,3 @@
         
-        private var sequence:SequenceRunner;
-        
-        [Before(async)]
+        [Before]
         public function initializeContext():void {
@@ -44,4 +41,2 @@
             
-            sequence = new SequenceRunner(this);
-            
             dodgyPerson = create(Person, {'id': 1, 'firstname': 'John001', 'lastname': 'Smith001', 'phone': 6803225});
@@ -59,8 +54,7 @@
             eventDispatcher = null;
-            sequence = null;
         }
-
+        
         [MessageError(type='com.darrenbishop.crm.directory.application.PersonEvent')]
         public function rethrowIt(processor:MessageProcessor, error:Error):void { 
-            eventDispatcher.dispatchEvent(new ErrorEvent(ErrorEvent.ERROR, false, false, error.message));
+            eventDispatcher.dispatchEvent(toEvent(error));
         }
@@ -75,6 +69,4 @@
             dispatcher(PersonEvent.newLookup(soundPerson.id));
-            
-            sequence.addStep(new SequenceWaiter(eventDispatcher, PersonEvent.FOUND, 1000));
-            
-            sequence.addAssertHandler(function(event:PersonEvent, data:*):void {
+            waitFor(eventDispatcher, PersonEvent.FOUND, 1000);
+            thenAssert(function(event:PersonEvent, data:*):void {
                 assertThat(event.type, equalTo('PersonEvent.found'));
@@ -82,4 +74,2 @@
             });
-            
-            sequence.run();
         }
@@ -89,10 +79,6 @@
             dispatcher(PersonEvent.newLookup(dodgyPerson.id));
-            
-            sequence.addStep(new SequenceWaiter(eventDispatcher, ErrorEvent.ERROR, 1000));
-            
-            sequence.addAssertHandler(function(event:ErrorEvent, data:*):void {
+            waitFor(eventDispatcher, 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)));
             });
-            
-            sequence.run();
         }
By counting the added (8) and subtracted (14) lines reveals a net reduction by 6 lines - a modest improvement. I could have indulged a bit more and come up with a nice Given, When, Then conforming DSL, but too much thought would have had to go into getting the semantics right, so I defer that for another day.

The important thing is I have achieved my goal, stated above.

async - refactored
So as discussed, all the Fluint Sequence usage has been pulled up into the AsyncHelper base class:
package com.darrenbishop.support.flexunit {
    import flash.events.ErrorEvent;
    import flash.events.Event;
    import flash.events.EventDispatcher;
    
    import mx.events.FlexEvent;
    
    import org.fluint.sequence.*;

    public class AsyncHelper {
        protected var sequence:SequenceRunner;
        
        [Before(async)]
        public function setUpSequence():void {
            sequence = new SequenceRunner(this);
        }
        
        [After(async)]
        public function tearDownSequence():void {
            sequence = null;
        }
        
        // Primitive steps
        
        protected function waitFor(dispatcher:EventDispatcher, eventType:String=FlexEvent.VALUE_COMMIT, timeout:Number=100):void {
            sequence.addStep(new SequenceWaiter(dispatcher, eventType, timeout));
        }
        
        protected function dispatch(dispatcher:EventDispatcher, event:Event):void {
            sequence.addStep(new SequenceEventDispatcher(dispatcher, event));
        }
        
        protected function assert(handler:Function, passThroughData:*):void {
            sequence.addAssertHandler(handler, passThroughData);
        }
        
        protected function run():void {
            sequence.run();
        }
        
        protected function toEvent(error:Error):ErrorEvent {
            return new ErrorEvent(ErrorEvent.ERROR, false, false, error.message);
        }
        
        // Composite steps
        
        protected function thenAssert(handler:Function, passThroughData:*=null):void {
            assert(handler, passThroughData || {});
            run();
        }
    }
}
Notice that this test base-class defines setup and teardown behaviour, using [Before] and [After] metadata tags. FlexUnit supports [Before] and [After] inheritance, using an accumulator strategy rather than the standard-OO override strategy:
  • [Before] annotated methods are applied from the root of the inheritance chain down, prior to a test method invocation
  • [After] annotated methods are applied from bottom to top
Presumably, this provides symmetry to the setup and teardown of state and fixtures, with each level of inheritance potentially relying on the effects of the levels above.

The AsyncHelper class is pretty self explanatory: the [Before] and [After] methods are nothing new - they just manage the existence of the sequence object. The rest of the methods just manipulate the sequence object, but again, in no new ways; they are given verb or verb-phrases for names, thus self-describing what they do.

The test author will need to know about these inherited methods to use them - not really a problem these days... we all hit Ctrl+Space, right?

The real value in DSLs comes when developers (or even non-developers, if you happen to pair with a QA guy or a BA) who are not the original author of the tests (or whatever) must read them; they will be able to discern the embedded flow/logic/intention/expectation a lot easier.

What's Next...

Part 4: Improved Parsley Support with FlexUnit's [RunWith(...)][Rule]
I'll introduce the ParsleyRunner, which facilitates integration of Parsley into FlexUnit testing, using the [RunWith] metadata tag. Also, with the recent release of FlexUnit 4.1, I'll implement improved Parsley-FlexUnit integration using the [Rule] metadata tag.

18 comments:

Michal Tesař said...

Hi,
I have made similar testCase as your's. But I am not able to catch dispatched event in the test case. I have created something like this... :

[MessageHandler(selector='testOk')]
public function passItOn(event:TestEvent):void {
eventDispatcher.dispatchEvent(event);
trace(event.toString());
}

[Test(async)]
public function tryIt():void {
dispatcher(new TestEvent(TestEvent.TEST_OK));
waitFor(eventDispatcher, TestEvent.TEST_OK, 1000);
thenAssert(function(event:TestEvent, data:*):void {
assertThat(event.type, equalTo('testOk'));
});
}


May be I did something wrong at event definition. My event:

package flexUnitTests.events
{
import flash.events.Event;

public class TestEvent extends Event
{
static public const TEST_OK :String = "testOk";

//public var OK:String = "ok";
public var testingEvent:Event;

public function TestEvent(type:String)
{
super(type);
}
}
}


Can you, please, help me and tell me what am I doing wrong?
My test every time fails:
Error: Timeout Occurred before expected event
.

Darren Bishop said...

Hi Michal, I've looked at your code and nothing looks wrong :-/
I've assumed you've taken the example code and made it work... then changed it to your needs?

Your event looks fine, except you have not overriden the clone() method; this could be the cause of your problem as the default impl will likely create an Event instead of TestEvent, which Parsley either won't recognise or won't (be able) to pass it as an arg to your passItOn(event:TestEvent).

Try that and comeback with a full code listing if it still does not work as teh code you've posted seems fine

Michal Tesař said...

Hi,
it doesn't help ...
I have uploaded my test project here - TestEvent.fxp
Can you, please, check my code and try it?

I've tried to create exactly same app as you did but I don't have classes such as StubDirectoryService etc...

Thanks for your help

Darren Bishop said...

Oh! Sorry, must have renamed by oversight :-/

Just go back to Part-2 and use the StubLookupTrackingDirectoryService code.

I'll have a look at your project when I can, but might not be for a while - would be easy if you post your test class code here. Then I can eyeball on my next tea break :-)

Michal Tesař said...

Hi,
my testclass: HandlingEventsTest.as

TestEvent.as

StubLookupTrackingDirectoryService wasn't only one missing part. I don't know how Person,GetPersonCommandComponentTestContext, classes look like...

Darren Bishop said...

I'll try to review these at some point today.

Meanwhile the Person class had id, firstname and lastname properties and a derived property fullname, which joins first and last.

The Parsley context you can compose yourself also; it/they just declare the StubDirectoryService and GetPersonCommand - remember Context you get for free.

Michal Tesař said...

Hi,

I've created Person class that is similar to your's but there isn't StubLookupTrackingDirectoryService doesn't have create method...

It should be really great if you can take a look at my code.

Darren Bishop said...

create(...) is just a global builder function for simple pofos; either use the Person constructor or setters.

The code for StubLookupTrackingDirectoryService is there in Part-2

Your context initialization is different and I am not familiar with the APIs you have used:

var context:Context = ContextBuilder.newSetup().newBuilder().config(FlexConfig.forClass(ParsleyConfig)).build();

Perhaps you are on the latest version of Parsley... I wrote this example against v2.2. Either way, if you can invoke dispatcher(...) without getting null errors, you context seems to be initialized.

Can you also verify that the command object is actually receiving and dispatching messages?

Michal Tesař said...

Hi,
my context builder create the Context correctly...
I can see that message was handled by
[MessageHandler(selector='TestEvent.ok')] - I receive trace of Event.toString() at passItOn method.

1.
Weird think is that it didn't trace from clone() method inside Event.
Can you ,please, check it in your project. Add trace to your clone method in event and tell me if it traced.

2.
If I add event listener to eventDispatcher :
eventDispatcher.addEventListener('TestEvent.ok',traceIt); And trace it at traceIt() method. I can see that event was dispatched by flex dispatcher too...
But SequenceWaiter inside waitFor doesn't recognize it :/

Darren Bishop said...

Hmmmn, I'm not sure mate.

How about you go back to Part-2 and do it all manually i.e. not using AsyncHelper but instead using the Fluint APIs directly.

Reapply your changes and see if the problem persists or goes away?

There's something to be said about the process of elimination :-/

Let me know...

Michal Tesař said...

Hi,
I've written test case with your part2...
But problem still persist..

If I change context definition than nothing change and only parsley inform me that this style of adding something to parsley context is old... So I think that problem is somewhere else.

There is my test class:
HandlingEventsTest_part2.as

If you create the same class as is mine do you have the same problem? Maybe there is some other settings that I should do... I'm still using this TestEvent.as. (If you do this you have to replace my new way of context definition to your's)...

Thanks for your time :)

Darren Bishop said...

Ok I got it!

Basically you are trying to do asynchronous testing by using [Test(async)] and the Fluint APIs, but you have no asynchronicity in your test.

You need to understand that using the Fluint APIs, through the async-DSL or otherwise, you are using the SequenceRunner stuff i.e. you build a sequence of steps that does not run until the end of your test, as in, when you call sequence.run() directly or indirectly through thenAssert(...).

In your test, however, you dispatch your event/message before you start building the sequence, so without any asynchronicity, the event is long-gone by the time the sequence is run and it just sits there waiting then timesout.

That's the problem - to see it for yourself, move your dispatch(...) call to after your call to thenAssert(...).

To fake some asynchronicity, leave the call to dispatch(...) at the top, but change it to dispatchLater(...) and add the following method:

protected function dispatchLater(event:Event, timeout:Number=500):void {
    setTimeout(dispatcher, timeout, event);
}

Michal Tesař said...

Hi,

thanks you. That's fantastic, it works :).

As you said, I had wrong order of calling methods. And call dispatcher() after build sequence solved my problem.

But I still don't understand your comment about async. If I don't add (async) to my [Test] meta tag, It fails after creating sequenceWaiter at AsyncLocator.as witch error message:
Error: Cannot add asynchronous functionality to methods defined by Test,Before or After that are not marked async
So it looks like I have asynchronicity in my testcase...

Once again thanks for your help :)

Darren Bishop said...

No, you marked your test with [Test(async)] and in doing so you've only indicated to FlexUnit that you want to test something that is asynchronous; typically this would be some network op e.g. calling a webservice.

(async) only tells FlexUnit not to consider the test to be over/complete when the test method returns... and that there is asynchronous stuff (webservice call) going on and it should wait for the assert handler/callback to be called before finishing (to avoid giving false positive or negatives).

So your problem is you aren't actually testing anything asynchronous - you're not making a webservice call, for example - everything happens in one shot: you first dispatch an event, which get's fully processed by any event handlers and then let go and probably even made good for GC; then you build your sequence and run it, but there's no event coming to set it all off.

Try the dispatchLater(...) approach - you should leave the event dispatching at the start of the test as it reads better and with asynchronicity, it will work.

Darren Bishop said...

And while investigating your issue, I realised I missed out the DirectoryService stub implementing the asychronicity in my example.

You did point out StubDirectoryService was missing, which is indeed different to StubLookupTrackingDirectoryService

My bad :-/

When I get a bit more time I will edit the post that introduces it and include a code listing.

I hope it's all clear for you now.

Darren Bishop said...

Checkout this GitHub gist for the missing files.

Michal Tesař said...

Hi,
it's all clear to me now.
Thanks for your help :)

ThomasB said...

@Darren

Nice work.

Have you checked out the Javascript testing framework jQuery QUnit.
Very cool and SUPER easy to use. Here is example test:
https://github.com/jquery/jquery/blob/master/test/unit/callbacks.js

Would be VERY cool if FlexUnit4 was this easy.

What is your twitter handle?