1

I am Unit Testing ViewModel in WPF application and there is a Delegate Command that calls a Method which further calls async method inside it . I have to wait for every Task to be finished before calling Assert statement. the Method called by the delegate command is like :

private void Methodcalled()
{
    this.uiService.SetBusyState();
    UIExecuteHelper executeHelper = new UIExecuteHelper(ViewName.Window);
    executeHelper.ExecuteAsync(() =>
        {
            // some stuff
             method1();
         }
}

Now I am waiting for the method in the following way in my unit test:

try
{
    var task = Task.Factory.StartNew(() =>
        {
            classobj.DelegateCommand.Execute();
        });
    var afterTask = task.ContinueWith((myobject)=>
        {
            classobj.Load();
            Assert.AreEqual(true, someflag);
        }, TaskContinuationOptions.OnlyOnRanToCompletion);

}

But it is still not waiting for all the inner tasks spawned to be finished. Please suggest

Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
priya
  • 852
  • 18
  • 39
  • What are you doing with `afterTask`, do you `afterTask.Wait()` or `await afterTask;` on it? – Scott Chamberlain Mar 24 '14 at 16:45
  • 2
    Ideally you should simply use a testing framework that is specifically designed for testing asynchronous methods. It would set up a message pump, handle tests that returned a `Task`, and validate the test based on the Task's result, rather than just the result of the test method. While you could put all of that framework in place yourself, you'd be better off letting the testing framework do that. – Servy Mar 24 '14 at 16:51
  • Why should I do aftertask.wait when i have applied ranTocompletion statement , this means the statements inside continuewith will executeonly when above method with complete , correct me if i am wrong. – priya Mar 24 '14 at 17:07
  • @Servy : I am unit testing viewmodels and using mbunit for that I am unaware of using seperate Framework for testing. please provide some links – priya Mar 24 '14 at 17:10
  • @priya I couldn't tell you if that framework has support for asynchronous methods, or of another similar framework that does. I can only tell you that something like this *should* be the responsibility of the framework. – Servy Mar 24 '14 at 17:13

4 Answers4

1

The underlying issue here is that the method that you are consuming is a fire and forget async method. See in particular Rule #4 "Libraries shouldn't lie" and rule #1 "For goodness' sake, stop using async void". It should really be returning a Task which is then awaitable in which case your unit test could await it properly and eliminate the need for the caller to do another Task.Run (preferred over Task.Factory.StartNew).

private Task Methodcalled()
{
    this.uiService.SetBusyState();
    UIExecuteHelper executeHelper = new UIExecuteHelper(ViewName.Window);
    return executeHelper.ExecuteAsync(() =>
        {
            // some stuff
             method1();
         }
}

Your unit test can then be done asynchronously (at least with MSTest):

[TestMethod]
public async Task TestItAsync()
{
   await Methodcalled();
   Assert.IsTrue(Something);
}

If you insist on using a fire and forget async pattern here you'll probably need to set a timer loop in your test to wait until the other thread is complete.

Jim Wooley
  • 10,169
  • 1
  • 25
  • 43
  • I can go with Timer loop but how to set a sufficient time after which i can assure that all the associated Tasks have completed – priya Mar 24 '14 at 18:05
1

there is a Delegate Command that calls a Method which further calls async method inside it.

@JimWooley correctly identified this as the root of the problem. One of the reasons to avoid async void is because async void methods are not (easily) testable.

The best solution is what he suggested:

  • Factor the actual logic of the command into a separate, Task-returning method.
  • Within the delegate command, just await that method.
  • Your unit tests can then call the async Task method rather than invoking the delegate command.

However, if you really insist on doing it the hard way, you'll need to install a custom SynchronizationContext and track the number of asynchronous operations. Or you can use my AsyncContext type which will do this for you:

await AsyncContext.Run(() => classobj.DelegateCommand.Execute());
classobj.Load();
Assert.AreEqual(true, someflag);
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • I solved the issue by creating a wrapper class that inherits synchronizationContext. Thanks for the hint. – priya Mar 25 '14 at 07:57
0

Besides that you should avoid async void in your unit tests (as pointed out by @JimWooley and @StephenCleary), you may also want to run the WPF-specific code on a WPF thread.

That's what I usually do, and putting this answer together is a good chance to improve some old code. A similar logic can be used with WinForms.

// The method to test
public static async Task IdleAsync(CancellationToken token)
{
    while (true)
    {
        token.ThrowIfCancellationRequested();
        await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);
        Console.WriteLine("Idle!");
    }
}

// The unit test method
[TestMethod]
public async Task TestIdleAsync()
{
    var cts = new CancellationTokenSource(1000);
    var task = RunOnWpfThreadAsync(() => IdleAsync(cts.Token));
    try
    {
        await task;
    }
    catch
    {
        if (!task.IsCanceled)
            throw;
    }
}

// non-generic RunOnWpfThreadAsync
public static Task RunOnWpfThreadAsync(Func<Task> funcAsync)
{
    // convert Task to Task<object>: http://stackoverflow.com/q/22541734/1768303
    return RunOnWpfThreadAsync(async () => {
        await funcAsync().ConfigureAwait(false); return Type.Missing; });
}

// generic RunOnWpfThreadAsync<TResult>
public static async Task<TResult> RunOnWpfThreadAsync<TResult>(Func<Task<TResult>> funcAsync)
{
    var tcs = new TaskCompletionSource<Task<TResult>>();

    Action startup = async () =>
    {
        // this runs on the WPF thread
        var task = funcAsync();
        try
        {
            await task;
        }
        catch
        {
            // propagate exception with tcs.SetResult(task)
        }
        // propagate the task (so we have the result, exception or cancellation)
        tcs.SetResult(task);

        // request the WPF tread to end
        // the message loop inside Dispatcher.Run() will exit
        System.Windows.Threading.Dispatcher.ExitAllFrames();
    };

    // the WPF thread entry point
    ThreadStart threadStart = () =>
    {
        // post the startup callback
        // it will be invoked when the message loop starts pumping
        System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke(
            startup, DispatcherPriority.Normal);
        // run the WPF Dispatcher message loop
        System.Windows.Threading.Dispatcher.Run();
    };

    // start and run the STA thread
    var thread = new Thread(threadStart);
    thread.SetApartmentState(ApartmentState.STA);
    thread.IsBackground = true;
    thread.Start();
    try
    {
        // propagate result, exception or cancellation
        return await tcs.Task.Unwrap().ConfigureAwait(false);
    }
    finally
    {
        // make sure the thread has fully come to an end
        thread.Join();
    }
}
noseratio
  • 59,932
  • 34
  • 208
  • 486
-1

why not use event handles (AutoResetEvent or ManualResetEvent) and then use WaitHande.WaitAll to block for all to finish. http://msdn.microsoft.com/en-us/library/z6w25xa6(v=vs.110).aspx

Dean Chalk
  • 20,076
  • 6
  • 59
  • 90
  • First off, there's no reason to do this. If you want to synchronously wait for the task to finish, just call `Wait` on it. Second, this can cause deadlocks if there is a synchronization context that the wait would end up blocking. – Servy Mar 24 '14 at 17:27
  • Then you're still failing when you should have just not deadlocked in the first place. When the testing framework is what's causing the deadlock, rather than the code in question, it's still wrong. – Servy Mar 24 '14 at 17:35
  • I have tried Autoreset , wait, waitAll but none worked. – priya Mar 24 '14 at 17:58