views:

273

answers:

4

I'm dipping my toes in how to test multi-threaded stuff, but not quite sure how to get started. I'm sure I will figure more stuff out easier if I could just get stuff going, so I was wondering if someone could help me write an NUnit test case for this simple class:

class Worker
{
    public event EventHandler<EventArgs> Done = (s, e) => { };

    public void StartWork()
    {
        var thread = new Thread(Work) { Name = "Worker Thread" };
        thread.Start();
    }

    private void Work()
    {
        // Do some heavy lifting
        Thread.Sleep(500);
        Done(this, EventArgs.Empty);
    }
}

What I would like to test is simply: Is the Done event raised when it finishes. I would have no problems if it was synchronous, but not sure where to even begin when it is not. A simple test if it wasn't multi-threaded (and the Work method wasn't private) could be:

[TestFixture]
class WorkerTests
{
    [Test]
    public void DoWork_WhenDone_EventIsRaised()
    {
        var worker = new Worker();

        var eventWasRaised = false;
        worker.Done += (s, e) => eventWasRaised = true;

        worker.Work();
        Assert.That(eventWasRaised);
    }
}

Any pointers?

+7  A: 

You need to use a ManualResetEvent - see Unit Testing Multi-Threaded Asynchronous Events for more details.

Something like:

[Test]
public void DoWork_WhenDone_EventIsRaised()
{
   var worker = new Worker();

   var eventWasRaised = false;
   var mre = new ManualResetEvent(false);
   worker.Done += (s, e) => { eventWasRaised= true; mre.Set(); };

   worker.Work();
   mre.WaitOne(1000);
   Assert.That(eventWasRaised);
}
mcintyre321
eventRaisedFlag can be omitted. It is simpler to use event as a flag. Assert.That(mre.WaitOne(1000));
Vadmyst
Oooh, now that's clever... brilliant!@Vadmyst: how would that work if say the event managed to finish before you call WaitOne?
Svish
Oh, I suppose thats why you'd use a ManualResetEvent instead of an AutoResetEvent?
Svish
+1  A: 

The main problem you find with testing threaded apps is actually stimulating the thread with test data because you will need to block on the main thread to wait until the other thread exits.

The way we've worked with this is to test it synchronously as you suggest. This allows you to test the logical behaviour but it won't detect deadlocks and race conditions of course (not that testing can assert these things easily anyway).

Matt Breckon
A: 

You can use a common pattern that exposes the thread creation to outer class.

In the class extract the thread creation to virtual method:

class Worker
{
    public event EventHandler<EventArgs> Done = (s, e) => { };

    public void StartWork()
    {
        var thread = CreateThread();
        thread.Start();
    }

    // Seam for extension and testability
    virtual protected Thread CreateThread()
    {
        return new Thread(Work) { Name = "Worker Thread" };
    }

    private void Work()
    {
        // Do some heavy lifting
        Thread.Sleep(500);
        Done(this, EventArgs.Empty);
    }
}

Define sub-class that exposes the thread:

class WorkerForTest : Worker
{
    internal Thread thread;

    protected override Thread CreateThread()
    {
        thread = base.CreateThread();
        return thread;
    }
}

Synchronize the test with the thread:

[TestFixture]
class WorkerTests
{
    [Test]
    public void DoWork_WhenDone_EventIsRaised()
    {
        var worker = new WorkerForTest();

        var eventWasRaised = false;
        worker.Done += (s, e) => eventWasRaised = true;

        worker.StartWork();

        // Use the seam for synchronizing the thread in the test
        worker.thread.Join();
        Assert.That(eventWasRaised);
    }
}

This case of design for testability has to advantages over synchronizing test thread by putting it to sleep before Assert:

  • It won't have false negative failures like could be when putting the test thread to sleep for period that usually is enough for worker thread to finish.
  • It won't run slower than it have to because sleep time takes buffer for making sure. This is important when many tests in the suite depend on it.
Elisha
A: 

There can be two options here: 1) Add a Wait method to the worker so that you can wait completion 2) Instead of simple boolean use event object (AutoResetEvent)

Generally, every wait has to wait for specified timeout. In the samples below wait is infinite.

First Option:

class Worker
{
 //...
    Thread thread;

    public void StartWork()
    {
        thread = new Thread(Work) { Name = "Worker Thread" };
        thread.Start();
    }

   void WaitCompletion()
   {
     if ( thread != null ) thread.Join(); 
   }
 //...
}

[TestFixture]
class WorkerTests
{
    [Test]
    public void DoWork_WhenDone_EventIsRaised()
    {
        var worker = new Worker();

        var eventWasRaised = false;
        worker.Done += (s, e) => eventWasRaised = true;

        worker.Work();
        worker.WaitCompletion();

        Assert.That(eventWasRaised);
    }
}

Second option: (Wait can be done with timeout)

[TestFixture]
class WorkerTests
{
    [Test]
    public void DoWork_WhenDone_EventIsRaised()
    {
        var worker = new Worker();

        AutoResetEvent eventWasRaised = new AutoResetEvent(false);
        worker.Done += (s, e) => eventWasRaised.Set();

        worker.Work();
        Assert.That(eventWasRaised.WaitOne());
    }
}
Vadmyst