简体   繁体   中英

How do I unit test a method containing an asynchronous call?

I have a method that contains an asynchronous call like this:

public void MyMethod() {
    ...
    (new Action<string>(worker.DoWork)).BeginInvoke(myString, null, null);
    ...
}

I'm using Unity and creating mock objects is not a problem, but how can I test that DoWork is called without worrying about race conditions?

A previous question offers a solution, but it seems to me that wait-handles is a hack (the race condition is still there, although it should be virtually impossible to raise).


EDIT: Okay, I thought I could ask this question in a very general manner, but it seems I have to elaborate further on the problem:

I want to create a test for the above mentioned MyMethod, so I do something like this:

[TestMethod]
public void TestMyMethod() {
   ...setup...
   MockWorker worker = new MockWorker();
   MyObject myObj = new MyObject(worker);
   ...assert preconditions...
   myObj.MyMethod();
   ...assert postconditions...
}

The naïve approach would be to create a MockWorker() that simply sets a flag when DoWork has been called, and test that flag in the postconditions. This would of course lead to a race condition, where the postconditions are checked before the flag is set in the MockWorker.

The more correct approach (which I'll probably end up using) is using a wait-handle:

class MockWorker : Worker {
    public AutoResetEvent AutoResetEvent = new AutoResetEvent();

    public override void DoWork(string arg) {
        AutoResetEvent.Set();
    }
}

...and use the following assertion:

Assert.IsTrue(worker.AutoResetEvent.WaitOne(1000, false));

This is using a semaphore-like approach, which is fine... but in theory the following could happen:

  1. BeginInvoke is called on my DoWork delegate
  2. For some reason neither the main-thread or the DoWork-thread is given execution time for 1000ms.
  3. The main-thread is given execution time, and because of the timeout the assertion fails, even though the DoWork thread is yet to be executed.

Have I misunderstood how AutoResetEvent works? Am I just being too paranoid, or is there a clever solution to this problem?

Wait handle would be the way I would do it. AFAIK (and I dont know for sure) asynchronous methods are using wait handles to fire off that method anyway.

I'm not sure why you think the race condition would occur, unless you are giving an abnormally short amount of time on the WaitOne call. I would put 4-5 seconds on the waitone, that way you'd know for sure if it was broken and it wasn't just a race.

Also, don't forget how wait handles work, as long as the wait handle is created, you can have the following order of execution a

  • Thread 1 - create wait handle
  • Thread 1 - set wait handle
  • Thread 2 - waitone on the wait handle
  • Thread 2 - blows through the wait handle and continues execution

Even though the normal execution is

  • Thread 1 - create wait handle
  • Thread 2 - waitone on the wait handle
  • Thread 1 - set wait handle
  • Thread 2 - blows through the wait handle and continues execution

Either works correctly, the wait handle can be set before Thread2 begins waiting on it and everything is handled for you.

When testing a straight delegate, just use the EndInvoke call to ensure the delegate is called and returns the appropriate value. For instance.

var del = new Action<string>(worker.DoWork);
var async = del.BeginInvoke(myString,null,null);
...
var result = del.EndInvoke(async);

EDIT

OP commented that they are trying to unit test MyMethod versus the worker.DoWork method.

In that case you will have to rely on a visible side effect from calling the DoWork method. Based on your example I can't really offer too much here because none of the inner workings of DoWork are exposed.

EDIT2

[OP] For some reason neither the main-thread or the DoWork-thread is given execution time for 1000ms.

This won't happen. When you call WaitOne on an AutoResetEvent the thread goes to sleep. It will recieve no processor time until the event is set or the timeout period expires. It's definitely pheasible that some other thread gets a significant time slice and causes a false failure. But I think that is fairly unlikely. I have several tests that run in the same manner and I don't get very many/any false failures like this. I usually pick a timeout though of ~2 minutes.

This is what I've done in these situations: Create a service that takes the delegate and executes it (expose this via a simple interface, inject where you call stuff asynchronously).

In the real service it will execute with BeginInvoke and thus asynchronously. Then create a version of the service for testing that invokes the delegate synchronously.

Here's an example:

public interface IActionRunner
{
   void Run(Action action, AsyncCallback callback, object obj);
   void Run<T>(Action<T> action, T arg, AsyncCallback callback, object obj);
   void Run<T1, T2>(Action<T1, T2> action, T1 arg1, T2 arg2, AsyncCallback callback, object obj);
   void Run<T1, T2, T3>(Action<T1, T2, T3> action, T1 arg1, T2 arg2, T3 arg3, AsyncCallback callback, object obj);
   void Run<T1, T2, T3, T4>(Action<T1, T2, T3, T4> action, T1 arg1, T2 arg2, T3 arg3, T4 arg4, AsyncCallback callback, object obj);
}

An Asycnhronous implementation of this service looks like this:

public void Run<T>(Action<T> action, T arg, AsyncCallback callback, object obj)
{
   action.BeginInvoke(arg, callback, obj);
}

A Synchronous implementation of this service looks like this:

public void Run<T>(Action<T> action, T arg, AsyncCallback callback, object obj)
{
   action(arg);
}

When you are unit testing, if you're using something like RhinoMocks and the AutoMocking container, you can swap in your Synchronous ActionRunner:

_mocks = new MockRepository();

_container = new AutoMockingContainer(_mocks);
_container.AddService(typeof(IActionRunner), new SyncActionRunner());
_container.Initialize();

The ActionRunner doesn't need to be tested; it's just a thin veneer over the method call.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM