public class ClassToTest { private readonly IMessageBus _messageBus; private CancellationTokenSource _cancellationTokenSource; public event EventHandler OnNewMessage; public ClassToTest(IMessageBus messageBus) { _messageBus = messageBus; } public void Start() { _cancellationTokenSource = new CancellationTokenSource(); Task.Run(() => { var message = _messageBus.GetNextMessage(); // Do work if (OnNewMessage != null) { OnNewMessage(this, EventArgs.Empty); } }, _cancellationTokenSource.Token); } public void Stop() { if (_cancellationTokenSource != null) { _cancellationTokenSource.Cancel(); _cancellationTokenSource = null; } } }When faced with similar a code a developer has a “tiny” problem – how to force the code inside Task.Run to execute before the end of the test is reached.
[TestMethod] public void BadTest() { var fakeMessageBus = A.Fake<IMessageBus>(); var wasCalled = false; var cut = new ClassToTest(fakeMessageBus); cut.OnNewMessage += (sender, args) => wasCalled = true; cut.Start(); Assert.IsTrue(wasCalled); }
[TestMethod] public void WorseTest() { var fakeMessageBus = A.Fake<IMessageBus>(); var wasCalled = false; var cut = new ClassToTest(fakeMessageBus); cut.OnNewMessage += (sender, args) => wasCalled = true; cut.Start(); Thread.Sleep(1000); Assert.IsTrue(wasCalled); }The problem is that by adding a sleep we managed to achieve an inconsistent test while also making sure that the test would run a staggering 1 second – if that doesn’t sound like a lot think how much time 60 of such tests would run, what about 1000 tests each taking a single second?
public class CurrentThreadTaskScheduler : TaskScheduler { protected override void QueueTask(Task task) { TryExecuteTask(task); } protected override bool TryExecuteTaskInline( Task task, bool taskWasPreviouslyQueued) { return TryExecuteTask(task); } protected override IEnumerable<Task> GetScheduledTasks() { return Enumerable.Empty<Task>(); } public override int MaximumConcurrencyLevel { get { return 1; } } }Now we need to pass that scheduler to the executing task either by using a wrapper object or dependency injection.
public void Start() { _cancellationTokenSource = new CancellationTokenSource(); Task.Factory.StartNew(() => { var message = _messageBus.GetNextMessage(); // Do work if (OnNewMessage != null) { OnNewMessage(this, EventArgs.Empty); } }, _cancellationTokenSource.Token, TaskCreationOptions.None, TaskScheduler.Current); }And now I could write the following test that would pass every single time.
[TestMethod] public void GoodTest() { var fakeMessageBus = A.Fake<IMessageBus>(); var wasCalled = false; Task.Factory.StartNew(() => { var cut = new ClassToTest(fakeMessageBus); cut.OnNewMessage += (sender, args) => wasCalled = true; cut.Start(); }, CancellationToken.None, TaskCreationOptions.None, new CurrentThreadTaskScheduler()); Assert.IsTrue(wasCalled); }Another benefit is that I could also test for negative conditions (something didn’t happen) since now all of the code runs synchronously.
Labels: C#, Concurrent, Tips and Tricks, Unit tests