amateur
amateur

Reputation: 44605

unit testing asynchronous operation

I want to unit test a method that I have that performs and async operation:

 Task.Factory.StartNew(() =>
        {
            // method to test and return value
            var result = LongRunningOperation();
        });

I stub the necessary methods etc in my unit test (written in c#) but the problem is that the async operation is not finished before I assert the test.

How can I get around this? Should I create a mock of the TaskFactory or any other tips to unit testing an async operation?

Upvotes: 14

Views: 8603

Answers (4)

Oyun
Oyun

Reputation: 198

Set UI and background task schedulars and replace them in unit test with this one.

Below code was copied from internet, sorry for missing reference to author:

  public class CurrentThreadTaskScheduler : TaskScheduler
  {
    protected override void QueueTask(Task task)
    {
      TryExecuteTask(task);
    }

    protected override bool TryExecuteTaskInline(
       Task task,
       bool taskWasPreviouslyQueued)
    {
      return TryExecuteTask(task);
    }

    protected override IEnumerable<Task> GetScheduledTasks()
    {
      return Enumerable.Empty<Task>();
    }

    public override int MaximumConcurrencyLevel => 1;
  }

So to test code:

   public TaskScheduler TaskScheduler
    {
      get { return taskScheduler ?? (taskScheduler = TaskScheduler.Current); }
      set { taskScheduler = value; }
    }

    public TaskScheduler TaskSchedulerUI
    {
      get { return taskSchedulerUI ?? (taskSchedulerUI = TaskScheduler.FromCurrentSynchronizationContext()); }
      set { taskSchedulerUI = value; }
    }
  public Task Update()
    {
      IsBusy = true;
      return Task.Factory.StartNew( () =>
                 {
                   LongRunningTask( );
                 }, CancellationToken.None, TaskCreationOptions.None, TaskScheduler )
                 .ContinueWith( t => IsBusy = false, TaskSchedulerUI );
    }

You will write following unit test:

[Test]
public void WhenUpdateThenAttributeManagerUpdateShouldBeCalled()
{
  taskScheduler = new CurrentThreadTaskScheduler();
  viewModel.TaskScheduler = taskScheduler;
  viewModel.TaskSchedulerUI = taskScheduler;
  viewModel.Update();
  dataManagerMock.Verify( s => s.UpdateData( It.IsAny<DataItem>>() ) );
}

Upvotes: 0

Jon Skeet
Jon Skeet

Reputation: 1499860

You'd have to have some way of faking out the task creation.

If you moved the Task.Factory.StartNew call to some dependency (ILongRunningOperationStarter) then you could create an alternative implementation which used TaskCompletionSource to create tasks which complete exactly where you want them to.

It can get a bit hairy, but it can be done. I blogged about this a while ago - unit testing a method which received tasks to start with, which of course made things easier. It's in the context of async/await in C# 5, but the same principles apply.

If you don't want to fake out the whole of the task creation, you could replace the task factory, and control the timing that way - but I suspect that would be even hairier, to be honest.

Upvotes: 7

SvenG
SvenG

Reputation: 239

I would propose to stub a TaskScheduler in your method with a special implementation for unit tests. You need to prepare your code to use an injected TaskScheduler:

 private TaskScheduler taskScheduler;

 public void OperationAsync()
 {
     Task.Factory.StartNew(
         LongRunningOperation,
         new CancellationToken(),
         TaskCreationOptions.None, 
         taskScheduler);
 }

In your unit test you can use the DeterministicTaskScheduler described in this blog post to run the new task on the current thread. Your 'async' operation will be finished before you hit your first assert statement:

[Test]
public void ShouldExecuteLongRunningOperation()
{
    // Arrange: Inject task scheduler into class under test.
    DeterministicTaskScheduler taskScheduler = new DeterministicTaskScheduler();
    MyClass mc = new MyClass(taskScheduler);

    // Act: Let async operation create new task
    mc.OperationAsync();
    // Act:  Execute task on the current thread.
    taskScheduler.RunTasksUntilIdle();

    // Assert
    ...
}

Upvotes: 5

Oblivion2000
Oblivion2000

Reputation: 616

Try something like this...

object result = null;
Task t =  Task.Factory.StartNew(() => result = LongRunningThing()); 


Task.Factory.ContinueWhenAll(new Task[] { t }, () => 
{
   Debug.Assert(result != null);
});

Upvotes: 0

Related Questions