Reputation: 158061
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?
Upvotes: 5
Views: 2701
Reputation: 13306
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);
}
Upvotes: 7
Reputation: 23780
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:
Upvotes: 1
Reputation: 1972
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());
}
}
Upvotes: 1
Reputation: 3374
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).
Upvotes: 1