In Part-2 I'll show how to implement a component-test, with some of the setup delegated to Parsley to leverage IoC and DI.
The 'test' code: revisited...
Again I'll jump straight into the test class itselfThe FlexUnit4 GetPersonCommandComponentTest
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 flash.events.ErrorEvent; import flash.events.EventDispatcher; import org.flexunit.assertThat; import org.fluint.sequence.SequenceRunner; import org.fluint.sequence.SequenceWaiter; 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 GetPersonCommandComponentTest { [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; private var sequence:SequenceRunner; [Before(async)] public function initializeContext():void { var ctx:Context = FlexContextBuilder.build(GetPersonCommandComponentTestContext); ctx.createDynamicContext().addObject(this); eventDispatcher = new EventDispatcher(); sequence = new SequenceRunner(this); 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; 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)); } [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)); sequence.addStep(new SequenceWaiter(eventDispatcher, PersonEvent.FOUND, 1000)); sequence.addAssertHandler(function(event:PersonEvent, data:*):void { assertThat(event.type, equalTo('PersonEvent.found')); assertThat(event.person.fullname, equalTo('John002 Smith002')); }); sequence.run(); } [Test(async,description='Test GetPersonCommand result-handler verifies the id of the person found.')] public function resultVerifiesPersonFound():void { dispatcher(PersonEvent.newLookup(dodgyPerson.id)); sequence.addStep(new SequenceWaiter(eventDispatcher, ErrorEvent.ERROR, 1000)); sequence.addAssertHandler(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(); } } }There's one quick thing to point out here - this component-test is actually simpler than the unit-test discussed in Part-1, with one fewer tests as there's no value in verifying the service-call-sequencing again. Incidentally, the remaining two tests still provide 75% coverage (for the same reasons as before).
Right, I'll breeze past dispatcher
, command
and service
; these are standard injections you'd use while programming with Parsley.
A lesser known fact about context
: no declaration is required for this dependency i.e. in GetPersonCommandTestContext
; Parsley will detect the expectation of an object of type org.spicefactory.parsley.core.context.Context
and will inject that instance in which this
is initialized by, that is, the context doing the injections will be injected.
Pretty neat, eh. I will show how to leverage this later in the series.
The next two variables, dodgyPerson
and soundPerson
, reflect a kind-of agreement between the StubDirectoryService
implementation that is injected and the test, which is something like: if you request dodgyPerson
, I will send back soundPerson
; I use this to implement the negative-test.
Again, mocking would be the better approach here, where interactions would then be dictated rather than just implied... moving on...
The eventDispatcher
is used here to bridge the Parsley Messaging Framework and FlexUnit (Fluint) Asynchronous Testing.
The sequence
object is the key to FlexUnit (Fluint) Asynchronous Testing; it must be used with the async
attribute in [Before]
, [After]
or [Test]
metadata tags.
Where the Magic Happens... Well, Some of It Anyway
initializeContext()
is where the Parsley context is initialized and this
test instance along with it.
[Before(async)] public function initializeContext():void { var ctx:Context = FlexContextBuilder.build(GetPersonCommandComponentTestContext); ctx.createDynamicContext().addObject(this); eventDispatcher = new EventDispatcher(); sequence = new SequenceRunner(this); 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; }From discussions I've had with my peers, it's the key insight and biggest stumbling block. Funnily enough, it's clearly documented here and here how to initialize a context programmatically, but I still needed help joinining the dots :-/
I borrowed those ideas and used them to implement a different approach:
- One (potentially) monolithic
FlexUnitApplicationContext
can be broken down to contain only what's relevant to each test - The test method states which context to use, making it more clear/traceable what is in the test environment (think:
Ctrl+click
) - Different contexts allow for different service (interface) implementations i.e. less injection-by-id
- Tools like FlashBuilder and Flexmojos are free to control (generate) the test application e.g.
FlexUnitApplication
Both eventDispatcher
and sequence
are initialized - nothing exciting - and destroyContext()
is also pretty straightforward.
Assignments to service.dodgyPerson
, dodgyPerson
service.soundPerson
and soundPerson
setup the agreement between the this
test and the service
.
Now let's review the rest of the code in method-pairs - this is is where the real magic happens...
Where the Real Magic Happens
FirstpassItOn(...)
and resultDoesNotManglePersonFound()
:
[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)); sequence.addStep(new SequenceWaiter(eventDispatcher, PersonEvent.FOUND, 1000)); sequence.addAssertHandler(function(event:PersonEvent, data:*):void { assertThat(event.type, equalTo('PersonEvent.found')); assertThat(event.person.fullname, equalTo('John002 Smith002')); }); sequence.run(); }Looking first at the test-method, you can quite easily read the sequence of events:
- I send off a person lookup request
- I wait for the person to be found
- I check the person is who I expect
dispatcher
i.e. using the Parsley Messaging Framework, yet the test is using Fluint (FlexUnit) to wait for the found notification i.e. using the plain-vanilla Flex Event Mechanism; the two technologies must be bridged - this is where passItOn(...)
comes in.
passItOn(...)
is adorned with the [MessageHandler]
metadata tag, which Parsley will detect during initialization and wire-up to its Messaging Framework. Now, the test instance will be informed when a person is found. When that happens, this method simply uses the eventDispatcher
to pass the found event onto the Flex Event Mechanism; where this eventDispatcher
is the same one used by Fluint, the test execution can now proceed.
Now for rethrowIt(...)
and resultVerifiesPersonFound()
:
[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)); } ... [Test(async,description='Test GetPersonCommand result-handler verifies the id of the person found.')] public function resultVerifiesPersonFound():void { dispatcher(PersonEvent.newLookup(dodgyPerson.id)); sequence.addStep(new SequenceWaiter(eventDispatcher, ErrorEvent.ERROR, 1000)); sequence.addAssertHandler(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(); }Again looking at the test-method first, you can see that:
- I send off a lookup request for the dodgy-person, thus triggering an error
- I wait for an error
- I check the error informs me of something remotely useful... or at least what the command thinks is useful
async
metadata-attribute and sequence
stuff... no bueno.
But then it dawned on me that error detection i.e. for [Test(expect=...)]
, needed errors to occur in the same call-stack, so that when the stack unwinds, FlexUnit can use a try-catch
block to detect and cross-check for the expect=...
and make an assertion on the expected error type.
That's my unverified rationalization for why it didn't work, anyway, and for adopting a similar messaging approach as before, hence rethrowIt(...)
. I had previously thought the error was getting lost, but I knew there was a way to respond to them.
rethrowIt(...)
is annotated with the [MessageError]
metadata tag, which again, Parsley will detect during initialization and wire-up. So whenever there is an error in a [MessageHandler]
or [CommandResult]
, Parsley will let the test know via rethrowIt(...)
.
Similar to passItOn(...)
, rethrowIt(...)
uses the eventDispatcher
to pass the error onto the Flex Event Mechanism; rethrowIt(...)
is a little bit different in that it must marshal the Error
object into a form the Flex Event Mechanism can understand, that is, an event and specifically a ErrorEvent
instance.
What's Next...
Part 3: Hiding Fluint Sequences with a Flow-based DSL
I'll introduce theAsyncHelper
base class and leverage FlexUnit's [Before]
and [After]
inheritance to abstract away all that Fluint Sequence stuff.
This approach makes available a bunch of helper methods that make asynchronous tests much easier to read.
No comments:
Post a Comment