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.
Hi Mike,
ReplyDeleteIn 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
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.
ReplyDeleteDaniel
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.
ReplyDeleteYou get a bool back from WaitOne indicating if the event has fired. So you can make your code a bit cleaner.
ReplyDeleteLike this:
[Test]
public void SampleTestWithAutoResetEvent()
{
var autoResetEvent = new AutoResetEvent(false);
new Timer(x =>
{
autoResetEvent.Set();
}, null, someAmountOfTime, Timeout.Infinite);
autoResetEvent.WaitOne(2000).ShouldBeTrue();
}
Thanks Anthony, good call :)
ReplyDeleteThis comment has been removed by the author.
ReplyDeleteI like using the concurrent collections. Avoids you having to write any 'threading' code:
ReplyDelete[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);
}
Thanks Jochen, that's a very nice idea.
ReplyDeleteThis comment has been removed by a blog administrator.
ReplyDelete