Tuesday, June 19, 2012

Use AutoResetEvent To Unit Test Multi-Threaded Code

I’ve been guilty of using a very nasty pattern recently to write unit (and integration) tests for EasyNetQ. Because I’m dealing with inherently multi-threaded code, it’s difficult to know when a test will complete. Up to today, I’d been inserting Thread.Sleep(x) statements to give my tests time to complete before the test method itself ended.

Here’s a contrived example using a timer:

[Test]
public void SampleTestWithThreadSleep()
{
var timerFired = false;

new Timer(x =>
{
timerFired = true;
}, null, someAmountOfTime, Timeout.Infinite);

Thread.Sleep(2000);
timerFired.ShouldBeTrue();
}

I don’t know what ‘someAmountOfTime’ is, so I’m guessing a reasonable interval and then making the thread sleep before doing the asserts. In most cases the ‘someAmountOfTime’ is probably far less than the amount of time I’m allowing. It pays to be conservative in this case :)

My excellent colleague Michael Steele suggested that I use an AutoResetEvent instead. To my shame, I’d never spent the time to really understand the thread synchronisation methods in .NET, so it was back to school for a few hours while I read bits of Joseph Albahari’s excellent Threading in C#.

An AutoResetEvent allows you to block one thread while waiting for another thread to complete some task; ideal for this scenario.

Here’s my new test:

[Test]
public void SampleTestWithAutoResetEvent()
{
var autoResetEvent = new AutoResetEvent(false);
var timerFired = false;

new Timer(x =>
{
timerFired = true;
autoResetEvent.Set();
}, null, someAmountOfTime, Timeout.Infinite);

autoResetEvent.WaitOne(2000);
timerFired.ShouldBeTrue();
}

If you create an AutoResetEvent by passing false into its constructor it starts in a blocked state, so my test will run as far as the WaitOne line then block. When the timer fires and the Set method is called, the AutoResetEvent unblocks and the assert is run. This test only runs for ‘someAmountOfTime’ not for the two seconds that the previous one took; far better all round.

Have a look at my EasyNetQ commit where I changed many of my test methods to use this new pattern.

8 comments:

Nikos Baxevanis said...

Hi Mike,

In general, if the wait time is short and unless multiple threads access the lock and thus resulting in contention you may use the ManualResetEventSlim class. There is a page on msdn spotting the differences at http://msdn.microsoft.com/en-us/library/5hbefs30.aspx

In .NET 4.0 there is also the SpinWait.SpinUntil method - an example of it can be found at http://goo.gl/6wV6j

Anonymous said...

I also suggest you use the slim version of the manualresetevent. You could then also use a cancellationtokensource in case of failures would this allow to cancel the source and the test wouldnt need to wait untill the timeout is up.

Daniel

Richard OD said...

Signalling constructs are your friends. In WPF MVVM, I often use the TPL, with that it is possible to change the TaskScheduler used so that in a unit test everything is Single Threaded and in the real code it uses the Threadpool as is the default behaviour.

Antony Denyer said...

You get a bool back from WaitOne indicating if the event has fired. So you can make your code a bit cleaner.

Like this:


[Test]
public void SampleTestWithAutoResetEvent()
{
var autoResetEvent = new AutoResetEvent(false);

new Timer(x =>
{
autoResetEvent.Set();
}, null, someAmountOfTime, Timeout.Infinite);

autoResetEvent.WaitOne(2000).ShouldBeTrue();
}

Mike Hadlow said...

Thanks Anthony, good call :)

Jochen Zeischka said...
This comment has been removed by the author.
Jochen Zeischka said...

I like using the concurrent collections. Avoids you having to write any 'threading' code:

[Fact]
public void CanReceive()
{
var publishedPing = new Ping {TestId = testId, MessageId = 345};
var receivedPings = new BlockingCollection();
Ping receivedPing;

using (IBus bus = RabbitHutch.CreateBus(ConnectionString))
{
// subscribe
bus.Subscribe("consumer", ping =>
{
// avoid receiving from previous tests
if (ping.TestId == testId)
receivedPings.Add(ping);
});

// publish
using (IPublishChannel publishChannel = bus.OpenPublishChannel())
publishChannel.Publish(publishedPing);

// wait for subscriber to receive message
receivedPings.TryTake(out receivedPing, 200.Milliseconds());
}

// assertions
receivedPing.Should().NotBeNull();
receivedPing.ShouldHave().AllProperties().EqualTo(publishedPing);
}

Mike Hadlow said...

Thanks Jochen, that's a very nice idea.