Monday, April 11, 2011

Darren on Flex: FlexUnit4 Testing with Parsley - Unit Testing Command Objects

In Part I, I'll start off by implementing a 'purist' unit-test. In order to do this, however, I need a context; I think a noddy CRM system will do - I was tempted to use a Calculator system, but decided against it as there was too much temptation to actually implement some logic.
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.

At the application-layer there is the:
  • DirectoryService interface, which will be stubbed and later used by Parsley to injection-by-type
  • GetPersonCommand, 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.
In the domain-layer there's the lonely 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 class
The 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 keys Person objects against their id
  • lookups, which is used to track the order in which the Command object makes person lookups... I know, it screams out for mocking, right
The 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:

psytranceismyreligion said...

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!