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
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 [RunWith(...)]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.