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.
DirectoryServiceinterface, 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 keysPersonobjects against theiridlookups, 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.