Sunday, December 29, 2013

Unit testing c# code which starts new threads via tasks

Unit testing multithreaded code seems sometimes impossible. We expect Unit Tests to run fast and to deliver predictable results. Tests which are executing code that spawns new threads are very often slow and fragile. These tests are not Unit Tests. Unit Tests are tests running our code in isolation from other parts of the system. Another part of the system are the classes providing access to the operating system multithreading capabilities. We use these classes to start new threads.
To test the functional part of our code with unit tests we have to exclude the concurrency and synchronization aspects. These aspects are system-wide concerns and have to be tested later separately with integration tests. Integration tests can for example put the system under load to uncover synchronisation problems between multiple threads.
But first we have to assure that the functional part of our code works as expected. We have to equip our code to be able to isolate the concurrency and synchronization aspects. Then we can write Unit Tests which are executing our functionality on the same thread as the unit test.
So for Unit Tests in conjunction with multithreaded code we can summarize:
  • Multithreaded code has to be equipped for isolating concurrency and synchronization aspects from functional aspects. 
  • Unit Tests can only test the functional aspect of multithreaded code. They can not test concurrency and synchronization aspects of the code, because these are concerns outreaching a single unit.
The jMock Team has described an brilliant approach for Java in the 'jMock Cookbook: Test Multithreaded Code'. This concept can also be applied to C# code using Tasks of the .NET Framework Class Libarary. The default TaskScheduler can be replaced with our own implementation called DeterministicTaskScheduler to test our code synchronously in a deterministic way. Unit tests are then able to run the tasks on their own thread:

[Test]
public void ShouldUpdateTheStatusWhenDoAsync()
{
    // Arrange 
    DeterministicTaskScheduler taskScheduler = new DeterministicTaskScheduler();
    SystemUnderTest sut = new SystemUnderTest(taskScheduler);
    Assert.AreEqual("Nothing done", sut.Status);
    
    // Act
    sut.DoAsync(); // starts a new task and returns immediately
    Assert.AreEqual("Starting task", sut.Status);
    // Now execute the new task
    taskScheduler.RunTasksUntilIdle();

    // Assert
    Assert.AreEqual("Task done", sut.Status);
}

Our code which starts a new task has to provide dependency injection capability for the TaskScheduler. We start a new task in DoAsync() via Task.Factory.StartNew():
public class SystemUnderTest
{
 private string status;
 private TaskScheduler taskScheduler;

 public SystemUnderTest(TaskScheduler taskScheduler)
 {
  status = "Nothing done";
  this.taskScheduler = taskScheduler;
 }

 public string Status {get { return status;}}

 public void DoAsync()
 {
  status = "Starting task";
  Task.Factory.StartNew(
      DoWork1, 
      new CancellationToken(), 
      TaskCreationOptions.None, 
      taskScheduler);
 }

 private void DoWork1()
 {
  // Do some work here
  status = "Task done";
 }
}

The DeterministicTaskScheduler to be used in unit tests:
    /// 
    /// TaskScheduker for executing tasks on the same thread that calls RunTasksUntilIdle() or RunPendingTasks() 
    /// 
    public class DeterministicTaskScheduler : TaskScheduler
    {
        private List<Task> scheduledTasks = new List<Task>(); 

        #region TaskScheduler methods

        protected override void QueueTask(Task task)
        {
            scheduledTasks.Add(task);
        }

        protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
        { 
            scheduledTasks.Add(task);
            return false;
        }

        protected override IEnumerable<Task> GetScheduledTasks()
        {
            return scheduledTasks;
        }

        public override int MaximumConcurrencyLevel { get { return 1; } }

        #endregion

        /// 
        /// Executes the scheduled Tasks synchronously on the current thread.
        /// If those tasks schedule new tasks they will also be executed
        /// until no pending tasks are left.
        /// 
        public void RunTasksUntilIdle()
        {
            while (scheduledTasks.Any())
            {
                this.RunPendingTasks();
            }
        }

        /// 
        /// Executes the scheduled Tasks synchronously on the current thread.
        /// If those tasks schedule new tasks they will only be executed 
        /// with the next call to RunTasksUntilIdle() or RunPendingTasks(). 
        /// 
        public void RunPendingTasks()
        {
            foreach (var task in scheduledTasks.ToArray())
            {
                this.TryExecuteTask(task);
                scheduledTasks.Remove(task);
            }
        }
    }

In the client production code we can inject a real TaskScheduler from the current context to our class:
SystemUnderTest sut = new SystemUnderTest(
                      TaskScheduler.FromCurrentSynchronizationContext());
sut.DoAsync();

See also my MSDN Magazine article for general remarks about unit testing multithreaded code.

3 comments:

Unknown said...

Hello, Sven. I'm very interested in learning about async testing. I don't really know how to solve a simple problem like this: imagine that my system under tests has a list of pending orders. When a client connects to the system, it can request the pending orders, but while the list is being retrieved, a new order is created, so the client will receive an obsolete version of the list.

How could I ensure that, if a "sync" operation is in progress, the client will receive the orders that are created in-between? And how to test this?

Thomas Maierhofer said...

I've written a concurrency test helper that uses threads to perform tests. My problem with tasks is the scheduler. In fact you don't know how the tasks are started and how many threads are used to perform the task. Here is the project page:
Crawler-Lib Concurrency Testing Helper

alicajohn said...

Great site. I add this Post to my bookmarks.
Arm Pain Frankfort
Abdominal Pain Frankfort
Back Pain Frankfort
Cancer Pain Management Frankfort
Chest Wall Pain Frankfort
Disc Herniation Frankfort