English 中文(简体)
C#: How to test a basic threaded worker class
原标题:

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?

最佳回答

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);
}
问题回答

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).

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());
    }
}

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.




相关问题
Anyone feel like passing it forward?

I m the only developer in my company, and am getting along well as an autodidact, but I know I m missing out on the education one gets from working with and having code reviewed by more senior devs. ...

NSArray s, Primitive types and Boxing Oh My!

I m pretty new to the Objective-C world and I have a long history with .net/C# so naturally I m inclined to use my C# wits. Now here s the question: I feel really inclined to create some type of ...

C# Marshal / Pinvoke CBitmap?

I cannot figure out how to marshal a C++ CBitmap to a C# Bitmap or Image class. My import looks like this: [DllImport(@"test.dll", CharSet = CharSet.Unicode)] public static extern IntPtr ...

How to Use Ghostscript DLL to convert PDF to PDF/A

How to user GhostScript DLL to convert PDF to PDF/A. I know I kind of have to call the exported function of gsdll32.dll whose name is gsapi_init_with_args, but how do i pass the right arguments? BTW, ...

Linqy no matchy

Maybe it s something I m doing wrong. I m just learning Linq because I m bored. And so far so good. I made a little program and it basically just outputs all matches (foreach) into a label control. ...

热门标签