Peter Goodman bio photo

Peter Goodman

A software engineer and leader living in Auckland building products and teams. Originally from Derry, Ireland.

Twitter Google+ LinkedIn Github

At first glance unit testing workflow activities with dependency injection seems like a no-brainer. For example consider the following activity using constructor-based dependency injection.

public class SendEmail : CodeActivity {
    private readonly IEmailClient emailClient;

    public SendEmail() : this(new EmailClient()) {}

    public SendEmail(IEmailClient emailClient) {
        this.emailClient = emailClient;
    }

    public InArgument<string> To { get; set; }

    public InArgument<string> From { get; set; }

    public InArgument<string> Subject { get; set; }

    public InArgument<string> Message { get; set; }

    protected override void Execute(CodeActivityContext context) {
        emailClient.SendEmail(
            To.Get(context),
            From.Get(context),
            Subject.Get(context),
            Message.Get(context)
            );
    }
}

public interface IEmailClient {
    void SendEmail(string to, string from, string subject, string message);
}

public class EmailClient : IEmailClient {
    public void SendEmail(string to, string from, string subject, string message) {
        throw new NotImplementedException();
    }
}

 

I have an email client but I choose to write my tests so that I can replace the implementation of IEmailClient such that I can stub or mock the implementation out as below. This is absolutely necessary for unit testing as we cannot have our real implementation of the Email Client sending emails when we run our unit tests. Therefore we swap out with a stub implementation that simply logs the fact that we have called SendEmail.

[TestMethod]
public void ExecuteCallsSendEmailOnClient() {
    StubEmailClient stubEmailClient = new StubEmailClient();
    WorkflowInvoker.Invoke(
        new SendEmail(
            stubEmailClient) {
                                       From = "foo@foo.com",
                                       To = "bar@bar.com",
                                       Subject = "The Subject",
                                       Message = "The Message"
                                   });

    Assert.AreEqual(1, stubEmailClient.SentEmails);
}

private class StubEmailClient : IEmailClient {
    private int sentEmails;
    public int SentEmails {
        get { return sentEmails; }
    }
    public void SendEmail(string to, string from, string subject, string message) {
        sentEmails++;
    }
}

 

So great! Now we have unit tested our activity and all is good with the world, no different from any other unit testing, right? But what if I have a workflow activity written in XAML that uses my SendEmail activity.

<Activity mc:Ignorable="sap"
          x:Class="PeteGoo.TimeService.Activities.SendMultipleEmails"
          xmlns="http://schemas.microsoft.com/netfx/2009/xaml/activities"
          xmlns:local="clr-namespace:PeteGoo.TimeService.Activities"
          xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
          xmlns:mva="clr-namespace:Microsoft.VisualBasic.Activities;assembly=System.Activities" 
          xmlns:sad="clr-namespace:System.Activities.Debugger;assembly=System.Activities"
          xmlns:sap="http://schemas.microsoft.com/netfx/2009/xaml/activities/presentation" 
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <x:Members>
    <x:Property Name="FromAddress"
                Type="InArgument(x:String)" />
    <x:Property Name="Subject"
                Type="InArgument(x:String)" />
    <x:Property Name="Message"
                Type="InArgument(x:String)" />
    <x:Property Name="ToAddresses"
                Type="InArgument(s:String[])" />
  </x:Members>
  <ForEach x:TypeArguments="x:String"
           DisplayName="ForEach&lt;String&gt;"
           sad:XamlDebuggerXmlReader.FileName="E:\Code\Blog\UnitTestingActivities\PeteGoo.TimeService.Activities\SendMultipleEmails.xaml"
           sap:VirtualizedContainerService.HintSize="287,206"
           Values="[ToAddresses]">
    <ActivityAction x:TypeArguments="x:String">
      <ActivityAction.Argument>
        <DelegateInArgument x:TypeArguments="x:String"
                            Name="toAddress" />
      </ActivityAction.Argument>
      <local:SendEmail From="[FromAddress]"
                       sap:VirtualizedContainerService.HintSize="257,100"
                       Message="[Message]"
                       Subject="[Subject]"
                       To="[toAddress]" />
    </ActivityAction>
  </ForEach>
</Activity>

Now suppose I want to test my XAML activity, how can I use dependency injection? XAML is now in control of the construction, so I cannot use constructor based Dependency Injection. This is a big problem as we really want to use the composition model that WF4 provides us and the declarative programming model of XAML but that stops us from using familiar unit testing techniques. Yes, there are other types of dependency injection but maybe workflow extensions can be useful here.Workflow Extensions allow us to provide common services in a Service Locator pattern built into the execution context. Typically they are used for Tracking Participants and Persistence Participants which are recognised as they implement a well known contract, in this case however we are going to take advantage of the fact that workflow extensions can be any arbitrary type. Consider the following workflow extension and the associated Extension method.

public class DependencyInjectionExtension {

    private readonly Dictionary<Type, object> dependencies = new Dictionary<Type, object>();
    public void AddDependency<T>(T instance) {
        dependencies.Add(typeof(T), instance);
    }

    public T GetDependency<T>(Func<T> defaultFunc) where T : class {
        if (dependencies.ContainsKey(typeof(T))) {
            return dependencies[typeof (T)] as T;
        }

        return defaultFunc();
    }
}

public static class ActivityContextExtensions {
    public static T GetDependency<T>(this ActivityContext context, Func<T> defaultFunc) where T : class {
        DependencyInjectionExtension extension = context.GetExtension<DependencyInjectionExtension>();
        
        return extension == null ? defaultFunc() : extension.GetDependency<T>(defaultFunc);
        
    }
}

 

Now we can use this extension and the extension method to change our Execute implementation to the following.

protected override void Execute(CodeActivityContext context) {
    IEmailClient client = context.GetDependency<IEmailClient>(() => new EmailClient());

    client.SendEmail(
        To.Get(context),
        From.Get(context),
        Subject.Get(context),
        Message.Get(context)
        );
}

 

This shows that we will ask the GetDependency extension method for a certain service (IEmailClient) and if it has not been specified simply use a new EmailClient instance. The extension method in turn, checks whether the extension has been added and if so asks it for the dependency. If the extension is not present or it does not contain a dependency for our type then the supplied func is called which will return that EmailClient instance. And our test is as follows:

[TestMethod]
public void ExecuteCallsSendEmailOnClient() {
    StubEmailClient stubEmailClient = new StubEmailClient();

    WorkflowInvoker invoker = new WorkflowInvoker(
        new SendEmail {
                                       From = "foo@foo.com",
                                       To = "bar@bar.com",
                                       Subject = "The Subject",
                                       Message = "The Message"
                                   
        });

    DependencyInjectionExtension dependencyInjectionExtension = new DependencyInjectionExtension();
    dependencyInjectionExtension.AddDependency<IEmailClient>(stubEmailClient);

    invoker.Extensions.Add(dependencyInjectionExtension);

    invoker.Invoke();

    Assert.AreEqual(1, stubEmailClient.SentEmails);
}

 

So now we have implemented a dependency injection pattern explicitly for workflow activities. We can now test composite XAML or code-based activities as well as ordinary code activities without requiring that we stay in control of the construction of our object instances.