Ironically, the point of this blog series is not about testing logic :-/
CRM It Is Then...
As mentioned in the intro, I've been working with Cairngorm3 and typically testing Parsley Command objects and Cairngorm-style PresentationModels (PMs).After applying the Cairngorm3 guidelines to the CRM system, I've structured the project as illustrated.
DirectoryService
interface, which will be stubbed and later used by Parsley to injection-by-typeGetPersonCommand
, which in this exercise will be the unit-under-test (UUT)- and
PersonEvent
, which will eventually be used with Parsley's Messaging Framework to implement low-coupling.
Person
class; I'll be making assertions against this and at some point during the series I'll demonstrate two neat ways to build and leverage test-fixtures.
The 'main' Code...
First up is the DirectoryService
interface
package com.darrenbishop.crm.directory.application { import mx.rpc.AsyncToken; public interface DirectoryService { function lookupById(id:uint):AsyncToken; } }Bit of a crappy CRM, with its one service call, but remember this is about testing the Command object, not service logic.
Next is the aforementioned UUT, the GetPersonCommand
This is a standard Command adhering to the execute-result-error
method naming convention. These methods will be auto-detected by Parsley if the command is declared as a DynamicCommand
; I'll use the [Metadata]
tags when it comes to it:
package com.darrenbishop.crm.directory.application { import com.darrenbishop.crm.directory.domain.Person; import mx.collections.ArrayCollection; import mx.rpc.AsyncToken; import mx.rpc.Fault; public class GetPersonCommand { public var dispatcher:Function; public var service:DirectoryService; public function execute(event:PersonEvent):AsyncToken { return service.lookupById(event.id); } public function result(person:Person, event:PersonEvent):void { if (person.id != event.id) { throw new Error(sprintf('Found person (%d) does not match lookup person (%d)', person.id, event.id)); } dispatcher(PersonEvent.newFound(person)); } public function error(f:Fault, event:PersonEvent):void { } } }You can see the
execute
method is a simple pass-through i.e. there is no outbound data-translation or encoding, etc. before the service call.
The result
method doesn't do any inbound data-translation, etc. but I do introduce a bit of verification logic: where the event
parameter is passed the original triggering event i.e. the same object received by the execute
method, I have access to the id
of the person requested; the person
parameter is passed the person looked-up and found by the would-be CRM system's directory-service. I use this data to verify I have actually found the person I am looking for... put another way, I've contrived a situation where I can throw an error; you'll see I will force this situation to implement a negative-test.
Last but not least, the PersonEvent
Events are re-purposed in Parsley as messages in its Messaging Framework - broadly doing the same thing as the standard event-mechanism, but offering better de-coupling, implementing a topic-subscription approach with message selectors
.
package com.darrenbishop.crm.directory.application { import com.darrenbishop.crm.directory.domain.Person; import flash.events.Event; public class PersonEvent extends Event { public static const LOOKUP:String = 'PersonEvent.lookup'; public static const FOUND:String = 'PersonEvent.found'; private var _id:uint; public function get id():uint { return _id; } private var _person:Person; public function get person():Person { return _person; } function PersonEvent(_:Guard, type:String) { super(type); } public static function newLookup(id:uint):PersonEvent { var event:PersonEvent = new PersonEvent(Guard.IT, LOOKUP); event._id = id; return event; } public static function newFound(person:Person):PersonEvent { var event:PersonEvent = new PersonEvent(Guard.IT, FOUND); event._person = person; return event; } public override function clone():Event { var event:PersonEvent = new PersonEvent(Guard.IT, type); event._id = id; event._person = person; return event; } } } class Guard { internal static const IT:Guard = new Guard(); }I use static factory methods here in-lieu of constructor-overloading - better anyway for removing confusion over what events are being constructed with what arguments. To restrict access to the sole constructor, I use a slight variation of the internal-Sentinel pattern that's quite common in Flex.
Anyway, that's it for the 'main' code, on to...
The 'test' Code...
I'll start with the actual test classThe FlexUnit4 GetPersonCommandUnitTest
This is a unit-test in the purist sense; all I'm interested in here is the behaviour of the command object, specifically, what goes on in the execute-result-error
methods. I feed the command canned-data and spy on it's outputs by controlling the objects it collaborates with. In this test case, those would be the service
and dispatcher
objects; the latter, which is a Function object, I use directly to make assertions, the former I inspect and assert on what I find.
package com.darrenbishop.crm.directory.application { import com.darrenbishop.crm.directory.domain.Person; import com.darrenbishop.support.create; import org.flexunit.assertThat; import org.flexunit.asserts.fail; import org.hamcrest.object.equalTo; public class GetPersonCommandUnitTest { public var service:StubLookupTrackingDirectoryService; public var command:GetPersonCommand; [Before] public function prepareCommand():void { service = new StubLookupTrackingDirectoryService(); command = new GetPersonCommand(); command.service = service; } [Test(description='Test GetPersonCommand calls the DirectoryService.lookupById(...) service correctly.')] public function lookUpByIdIsCalledCorrectly():void { service.add(create(Person, {'id': 001, 'firstname': 'John001', 'lastname': 'Smith001', 'phone': 6977150})); service.add(create(Person, {'id': 002, 'firstname': 'John002', 'lastname': 'Smith002', 'phone': 6809168})); service.add(create(Person, {'id': 003, 'firstname': 'John003', 'lastname': 'Smith003', 'phone': 7208442})); command.execute(PersonEvent.newLookup(002)); command.execute(PersonEvent.newLookup(001)); command.execute(PersonEvent.newLookup(003)); assertThat(service.nextLookup().fullname, equalTo('John002 Smith002')); assertThat(service.nextLookup().fullname, equalTo('John001 Smith001')); assertThat(service.nextLookup().fullname, equalTo('John003 Smith003')); } [Test(description='Test GetPersonCommand result-handler does not mangle the person found.')] public function resultDoesNotManglePersonFound():void { command.dispatcher = function(event:PersonEvent):void { assertThat(event.person.fullname, equalTo('John002 Smith002')); }; var person:Person = create(Person, {'id': 002, 'firstname': 'John002', 'lastname': 'Smith002', 'phone': 6809168}); command.result(person, PersonEvent.newLookup(002)); } [Test(expects='Error',description='Test GetPersonCommand result-handler verifies the id of the person found.')] public function resultVerifiesPersonFound():void { command.dispatcher = function(event:PersonEvent):void { }; var person:Person = create(Person, {'id': 001, 'firstname': 'John002', 'lastname': 'Smith002', 'phone': 6809168}); command.result(person, PersonEvent.newLookup(002)); } } }I've implemented three tests, including the one negative test, that gives 75% branch coverage; testing the
error
method is just the same as testing the result
method, so I've skipped it.
Some might wonder why I don't use mocking to stub/mock the DirectoryService
- I really ought to have but the integration options of, say mockito-flex (my favourite), are not available to me: I can't extend MockitoTestCase
or use the MockitoClassRunner
as in the next parts I use both mechanisms to implement the Parsley integration and DSL support.
On that note, here's the code for the stubbed DirectoryService
...
Faking It: The StubLookupTrackingDirectoryService
package com.darrenbishop.crm.directory.application { import com.darrenbishop.crm.directory.domain.Person; import flash.utils.Dictionary; import mx.rpc.AsyncToken; public class StubLookupTrackingDirectoryService implements DirectoryService { private var people:Dictionary; private var lookups:Array; public function StubLookupTrackingDirectoryService() { people = new Dictionary(); resetLookups(); } public function resetLookups():void { lookups = []; } public function add(person:Person):void { if (people[person.id]) { throw new Error('A person with id ' + person.id + ' already exists.'); } people[person.id] = person; } public function nextLookup():Person { return lookups.shift(); } public function lookupById(id:uint):AsyncToken { lookups.push(people[id]); return null; } } }You can see the stub has two variables:
people
, a dictionary which keysPerson
objects against theirid
lookups
, which is used to track the order in which the Command object makes person lookups... I know, it screams out for mocking, right
people
dictionary was originally populated with a dozen or so Person
objects; I refactored this as having the test fixture combined into the stub isn't so good. I pushed it out to the test methods, which is a better approach, providing self documenting tests; from reading the test method I know exactly: what reference-data I'm priming the system with i.e. the Person
objects; what test-data I'm activating the system with i.e. the id
to lookup with; what output-data I expect i.e. the found person. Much better.
What's Next...
Part 2: Asynchronous Testing with Parsley and Fluint Sequences
I'll use FlexUnit4's[Before]
feature to build a Parsley context and dynamically initialize the test class with references to the object-under-test (OUT) and the OUT's dependencies.
I'll also show using Fluint Sequences to test a complete asynchronous flow through the Command object.
1 comment:
Hi Darren,
this series looks great but it would be made a lot more useful if you could supply the full source code for all the classes.
Any chance you can publish it?
Thanks!
Post a Comment