Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Mocking Task.Factory.StartNew

0.00/5 (No votes)
6 Jan 2014 1  
Task.Factory.StartNew can be very useful for executing a method on another thread where you don't care about the result, but it can also make the host method hard to test. This tip describes how to inject Task.Factory and mock it for testing.

Introduction

This tip illustrates how the .NET Task.Factory.StartNew(Task) method can be injected as a dependency and mocked for testing. The sample project includes a Test project using nUnit and Rhino Mocks to show the code working, but any unit test and mocking framework can be used to achieve the same result.

Background

On a recent project, I had the requirement to execute calls to an external email subscription service using a separate thread. We did this to improve the user experience. The user didn't need to know if the email trigger was successful or not, so there was no point making the user wait. Also, because we were calling a third party web service, we couldn't guarantee the response time. To solve this, I used the Task.Factory class to start a new task to send the service message to the external web service, but not wait for the response (effectively a fire and forget). The task was responsible for logging any errors.

This all worked well in a proof of concept, but when I started coding the real solution I immediately ran into a problem. I'm a big fan of test driven development, but the call to Task.Factory.StartNew(Action) was making the method hard to test. After trawling the net for a solution, I stumbled across various articles and blogs about using a wrapping interface (some called it a factory, but the final solution I chose is more like a proxy). That solved the problem of injecting the Task.Factory.StartNew(Task) as a dependency. I then also found a solution to use mocks to verify that the correct action is being called. So I can't take credit for the solution below, but I thought it useful to present the two parts together, as they form the total solution (or did in my case).

Creating a Proxy

The first step to making the Task.Factory.StartNew(Task) injectable is to wrap it with our own interface and accompanying implementation. This technique is similar to my previous article on the Proxy Pattern (see http://www.codeproject.com/Articles/664496/Using-the-Proxy-Pattern-to-inject-the-uninjectable). Our simple interface exposes a method to start a new task on a separate thread.

public interface ITaskFactory
{
    void StartTask(Action action);
}  

Next we implement the interface.

public class TaskFactory : ITaskFactory
{
    public void StartTask(Action action)
    {
        Task.Factory.StartNew(action);
    }
}

Injecting the Proxy

Now that we have an interface for our task factory, we can inject it into a class that needs it. The following class also has a secondary class injected. The secondary class and method (ClassTwo.FireAndForget(int)) will be used as the action for the new task.

public class ClassOne : IClassOne
{
    private readonly ITaskFactory _taskFactory;
    private readonly IClassTwo _classTwo;
 
    public ClassOne(ITaskFactory taskFactory, IClassTwo classTwo)
    {
        _taskFactory = taskFactory;
        _classTwo = classTwo;
    }
 
    public void CallAsync(int number)
    {
        _taskFactory.StartTask(() => _classTwo.FireAndForget(number));
 
        Console.WriteLine("Response from ClassOne with {0} on thread {1}.", number,
            Thread.CurrentThread.ManagedThreadId);
    }
}

For illustration purposes only, I've added some Console.WriteLine statements that represent the work that the method is doing after the task is started.

Unit Testing

Now I want to unit test ClassOne.CallAsync. I can make use of the ITaskFactory interface to mock the StartTask method. I also want to test that ClassTwo.FireAndForget(int) is the action that will be executed in the new task and that the parameter was passed into correctly. The code below shows the full test class to test these two scenarios.

[TestFixture]
public class ClassOneTest
{
    #region SetUp
 
    private IClassOne _classOne;
    private ITaskFactory _taskFactory;
    private IClassTwo _classTwo;
 
    [SetUp]
    public void SetUp()
    {
        // mock the TaskFactory
        _taskFactory = MockRepository.GenerateStub<itaskfactory>();
 
        // mock the class we want to call asynchronously
        _classTwo = MockRepository.GenerateStub<iclasstwo>();
 
        // setup system under test
        _classOne  = new ClassOne(_taskFactory, _classTwo);
    }
 
    #endregion
 
    #region CallAsync
 
    [Test]
    public void CallAsyncCallsTaskFactory()
    {
        // system under test
        _classOne.CallAsync(1);
 
        // assertions
        _taskFactory.AssertWasCalled(t=>t.StartTask(Arg<action>.Is.Anything));
    }
 
    [Test]
    public void CallAsyncCallsClassTwoByTaskFactory()
    {
        // setup
        Action actionExpected = null;
        _taskFactory.Expect(x => x.StartTask(Arg<action>.Is.Anything))
                    .WhenCalled(y => actionExpected = (Action)y.Arguments[0]);
 
        // system under test
        _classOne.CallAsync(1);
        actionExpected.Invoke();
 
        // assertions
        _classTwo.AssertWasCalled(e => e.FireAndForget(Arg<int>.Is.Equal(1)));
    }
 
    #endregion
}

The SetUp() method creates the mock objects for the dependencies. As mentioned above, we're using Rhino Mocks as our mocking framework. The first test, CallAsyncCallsTaskFactory(), verifies (asserts) that a new task is started when CallAsync is executed. The second test verifies that FireAndForget is the method that will be executed by taking advantage of Rhino Mocks WhenCalled extension. This allows us to retrieve the first argument passed into the TaskFactory mock (which is the action being executed).

The action is the FireAndForget method on the mock we setup for the ClassTwo object, so when we call Invoke() on that action, the mock will allow us to assert that the FireAndForget method is executed and also verify that the parameter passed into it is correct.

History

  • 7 January 2014: Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here