views:

97

answers:

4

I have an external library which has a method which performs a long running task on a background thread. When it's done it fires off a Completed event on the thread that kicked off the method (typically the UI thread). It looks like this:

public class Foo
{
    public delegate void CompletedEventHandler(object sender, EventArgs e);
    public event CompletedEventHandler Completed;

    public void LongRunningTask()
    {
        BackgroundWorker bw = new BackgroundWorker();
        bw.DoWork += new DoWorkEventHandler(bw_DoWork);
        bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
        bw.RunWorkerAsync();
    }

    void bw_DoWork(object sender, DoWorkEventArgs e)
    {
        Thread.Sleep(5000);
    }

    void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if (Completed != null)
            Completed(this, EventArgs.Empty);
    }
}

The code that calls this library looks like this:

private void button1_Click(object sender, EventArgs e)
{
    Foo b = new Foo();
    b.Completed += new Foo.CompletedEventHandler(b_Completed);
    b.LongRunningTask();

    Debug.WriteLine("It's all done");    
}

void b_Completed(object sender, EventArgs e)
{
    // do stuff
}

How do I unit test the call to .LongRunningTask given that it returns data in an event?

+1  A: 

Well, I believe BackgroundWorker uses the current SynchronizationContext. You could potentially implement your own subclass of SynchronizationContext to allow you more control (possibly even running code on the same thread, although that will break anything which depends on it running in a different thread) and call SetSynchronizationContext before running the test.

You'd need to subscribe to the event in your test, and then check whether or not your handler was called. (Lambda expressions are good for this.)

For example, suppose you have a SynchronizationContext which lets you run all the work only when you want it to, and tell you when it's done, your test might:

  • Set the synchronization context
  • Create the component
  • Subscribe to the handler with a lambda which sets a local variable
  • Call LongRunningTask()
  • Verify that the local variable hasn't been set yet
  • Make the synchronization context do all its work... wait until it's finished (with a timeout)
  • Verify that the local variable has now been set

It's all a bit nasty, admittedly. If you can just test the work it's doing, synchronously, that would be a lot easier.

Jon Skeet
+1  A: 

You can create an extension method that can help with turning it into a synchronous call. You can make tweaks like making it more generic and passing in the timeout variable but at least it will make the unit test easier to write.

static class FooExtensions
{
    public static SomeData WaitOn(this Foo foo, Action<Foo> action)
    {
        SomeData result = null;
        var wait = new AutoResetEvent(false);

        foo.Completed += (s, e) =>
        {
            result = e.Data; // I assume this is how you get the data?
            wait.Set();
        };

        action(foo);
        if (!wait.WaitOne(5000)) // or whatever would be a good timeout
        {
            throw new TimeoutException();
        }
        return result;
    }
}

public void TestMethod()
{
    var foo = new Foo();
    SomeData data = foo.WaitOn(f => f.LongRunningTask());
}
aqwert
+1  A: 

For testing asynchronous code I use a similar helper:

public class AsyncTestHelper
{
    public delegate bool TestDelegate();

    public static bool AssertOrTimeout(TestDelegate predicate, TimeSpan timeout)
    {
        var start = DateTime.Now;
        var now = DateTime.Now;
        bool result = false;
        while (!result && (now - start) <= timeout)
        {
            Thread.Sleep(50);
            now = DateTime.Now;
            result = predicate.Invoke();
        }
        return result;
    }
}

In the test method then call something like this:

Assert.IsTrue(AsyncTestHelper.AssertOrTimeout(() => changeThisVarInCodeRegisteredToCompletedEvent, TimeSpan.FromMilliseconds(500)));
trendl