Having covered the vision and napkin design of an automation framework for our product's core services, it's time for a working end-to-end example.  My goal is to be able to drive one function of our core product: creating a user account.  In addition, I will drive it from both PowerShell and FitNesse to see how well the framework meets the needs from the initial vision.

Getting to Red

I broke ground with this test:

[Test] 
public void CreateUserAccountCommandExecution()
{
    ICommand command = new CreateUserAccountCommand { Name = "joe" }; 
    bool result = command.Execute(); 
    Assert.IsTrue( result ); 
} 

Simple enough - a textbook command pattern; note:

  • an ICommand interface defines the command contract;
  • at the moment, the only member of ICommand is an Execute() method.  It accepts no arguments and returns a boolean to indicate success or failure;
  • CreatePlayerAccountCommand is a concrete implementation of the ICommand contract;
  • CreatePlayerAccountCommand has a Name property that identifies the user name.

Getting to Green

First thing's first - I need the command contract:

public interface ICommand
{ 
    bool Execute(); 
} 

Then I can implement the concrete CreatePlayerAccountCommand type:

public class CreateUserAccountCommand : ICommand 
{ 
    public string Name { get; set; }  
    public bool Execute() 
    { 
        IUserService clientInterface = new RemoteUserService( "http://beefarino:8089" );  
        Credentials credentials = new Credentials( "user-manager", "password" ); 
        Ticket authTicket = clientInterface.Authenticate( credentials );  
        UserData userProperties = new UserData();  
        userProperties.FirstName = Name; 
        userProperties.LastName = "Smyth"; 
        userProperties.Nickname = Name; 
        userProperties.DateOfBirth = System.DateTime.Now - TimeSpan.FromDays( 365.0d * 22.0d );
         
        string userId = clientInterface.CreateUser( authTicket, userProperties );  
        Ticket userTicket; 
        clientInterface.CreateUserTicket( authTicket, userId,  out userTicket );  
        return null != userTicket;
    } 
} 

I'm not going to discuss this code except to explain that:

  • the logic in the Execute() method performs the minimum amount of activity necessary to create a user account;
  • I'm making assumptions about a lot of the data I need (e.g., the age of the user).  I'm trying to keep the command as simple as unconfigurable as possible, and there are many, many more UserData fields available for account configuration that I'm not using;
  • the command object method does nothing outside of it's intended scope: it creates a user account, that's it.

Use it from PowerShell

Now that I have the command working, I want to see it working in PowerShell.  I'm taking a minimalist approach starting out.  Once I implement a few more commands and plug them into PowerShell, I'll see what implementation patterns emerge and replace this approach with something cleaner.  But for now, this mess will do:

[System.Reflection.Assembly]::LoadFrom( 'automation.commands.dll' ); 
function new-useraccount() 
{ 
    param( [string] name ); 
     
    $cmd = new-object automationcommands.createuseraccountcommand; 
    $cmd.Name = $name;
    $cmd.Execute(); 
}  
new-useraccount -name 'scott'; 

Hmmm ... runs silent, no output ... but looking at the system backend, I can see that it works.  

Use it from FitNesse

I downloaded the latest stable version of FitNesse from http://www.fitnesse.org/ and followed Cory Foy's short tutorial on using it against .NET assemblies (which is still accurate after 3+ years, #bonus) to get things running.  I created a new page and entered the following wikitext and table:

!contents -R2 -g -p -f -h 
!define COMMAND_PATTERN {%m %p} 
!define TEST_RUNNER {dotnet\FitServer.exe} 
!define PATH_SEPARATOR {;} 
!path dotnet\*.dll 
!path C:\dev\spikes\Automation\PokerRoom.Fixtures\bin\Debug\pokerroom.fixtures.dll  
A simple test of the CreateUser command: 
|!-PokerRoom.Fixtures.CreateUserAccount-!| 
|name|created?| 
|phil|true| 
|bob|true| 
|alice|true| 

I hacked up a quick fixture to support the table...

namespace PokerRoom.Fixtures 
{ 
    public class CreateUserAccount : fit.ColumnFixture 
    { 
        public string name { get; set; }  
        public bool created() 
        { 
            ICommand cmd = new CreateUserAccountCommand { Player = name };
    
            return cmd.Execute();
        } 
    } 
} 

... build it, and the FitNesse tests are green ...

After verifying that the users are actually created in the live system using our proprietary tools, I'm satisfied.

Moving Forward

So far so good.  It's very dirty, but it's working.  w00t * 2!

While developing this today I noted a few areas of concern:

  1. In the command object, there are several dependencies that obviously should to be injected.  Namely, the IUserService instance and the authority credentials;
  2. These dependencies are only really needed in the Execute() method;
  3. Looking ahead, I know I'm going to have many of these services, and it will be a pain to inject them all for each command instantiation;
  4. Compositing commands into complex behavior will eventually lead to the need to share state between commands.  I have an idea of how to manage this, but I'm concerned it will be cumbersome;
  5. There needs to be some kind of feedback when using the command from PowerShell; not sure where this should live or what it should look like at the moment...
  6. PowerShell will have a lot more to offer if I integrate with it more deeply.  I'll have to think about what this will look like, so as to minimize the amount of custom scripting necessary to run commands while accessing the full PowerShell feature set;
  7. I need to learn a lot more about FitNesse :).  I've already given the elevator speech to a coworker and demonstrated the fixture - he had a lot more questions than I had answers...

My next few posts will detail how I address these and other concerns.  Next post will detail some prefactoring to take care of items 1-4, maybe demonstrate command compositing.