http://helpercode.com
concurrentcode
Demos: https://github.com/dhelper/ConcurrentUnitTesting
Consultant & software architect @ Practical Software
Developing software since 2002
Clean Coder & Test-Driven Developer
Pluralsight author
B: http://helpercode.com
T: @dhelper
About.ME
The free lunch is over!
Multi-core CPUs are the new standard
New(er) language constructs
New(ish) languages
We live in a concurrent world!
[Test]
public void AddTest()
{
var cut = new Calculator();
var result = cut.Add(2, 3);
Assert.AreEqual(5, result);
}
A good
unit test
must be:
Trustworthy
Maintainable
Readable
smells
×Inconsistent results
× Untraceable fail
× Long running tests
× Test freeze
public void Start() {
_worker = new Thread(() => {
while (_isAlive) {
Thread.Sleep(1000);
var msg = _messageProvider.GetNextMessage();
//Do stuff
LastMessage = msg;
}
});
_worker.Start();
}
[TestMethod]
public void ArrivingMessagePublishedTest()
{
var fakeMessageProvider = A.Fake<IMessageProvider>();
A.CallTo(() => fakeMessageProvider.GetNextMessage()).Returns("Hello!");
var server = new Server(fakeMessageProvider);
server.Start();
Thread.Sleep(2000);
Assert.AreEqual("Hello!", server.LastMessage);
}
Sleep
× Time based - fail/pass inconsistently
× Test runs for too long
× Hard to investigate failures
“
”
can happen
it will
[TestMethod]
public async Task ArrivingMessagePublishedTest()
{
var fakeMessageProvider = A.Fake<IMessageProvider>();
A.CallTo(() => fakeMessageProvider.GetNextMessage()).Returns("Hello!");
var server = new Server(fakeMessageProvider);
server.Start();
await Task.Delay(2000);
Assert.AreEqual("Hello!", server.LastMessage);
}
Solution: avoid concurrent code!
Code under Test
Start
Humble object
Async
Perform action
Perform Action
Assert Result
Production
Code http://xunitpatterns.com/Humble%20Object.html
public void Start() {
_worker = new Thread(() => {
while (_isAlive) {
Thread.Sleep(1000);
var msg = _messageProvider.GetNextMessage();
//Do stuff
LastMessage = msg;
}
});
_worker.Start();
}
public void Start() {
_worker = new Thread(() => {
while (_isAlive) {
Thread.Sleep(1000);
_messageHandler.HandleNextMessage();
}
});
_worker.Start();
}
[TestMethod]
public void ArrivingMessagePublishedTest()
{
var fakeMessageProvider = A.Fake<IMessageProvider>();
A.CallTo(() => fakeMessageProvider.GetNextMessage()).Returns("Hello!");
var messageHandler = new MessageHandler(fakeMessageProvider);
messageHandler.HandleNextMessage();
Assert.AreEqual("Hello!", messageHandler.LastMessage);
}
public class MessageManager
{
private IMesseageQueue _messeageQueue;
public void CreateMessage(string msg)
{
// Here Be Code!
_messeageQueue.Enqueue(message);
}
}
public class MessageClient
{
private IMesseageQueue _messeageQueue;
public string LastMessage { get; set; }
private void OnMsg(object o, EventArgs e)
{
// Here Be Code!
LastMessage = e.Message;
}
}
Start
Code under test
Async
Logic2
Assert results
Logic1
Start Logic1 Assert ResultsFake
Start Logic2 Assert ResultsFake
[TestMethod]
public void AddNewMessageProcessedMessageInQueue()
{
var messeageQueue = new AsyncMesseageQueue();
var manager = new MessageManager(messeageQueue);
manager.CreateNewMessage("a new message");
Assert.AreEqual(1, messeageQueue.Count);
}
[TestMethod]
public void QueueRaisedNewMessageEventClientProcessEvent()
{
var messeageQueue = new AsyncMesseageQueue();
var client = new MessageClient(messeageQueue);
client.OnMessage(null, new MessageEventArgs("A new message"));
Assert.AreEqual("A new message", client.LastMessage);
}
The best possible solution
No concurrency == no problems
× Do not test some of the code
× Not applicable in every scenario
public class ClassWithTimer
{
private Timer _timer;
public ClassWithTimer(Timer timer)
{
_timer = timer;
_timer.Elapsed += OnTimerElapsed;
_timer.Start();
}
private void OnTimerElapsed(object sender, ElapsedEventArgs e)
{
SomethingImportantHappened = true;
}
public bool SomethingImportantHappened { get; private set; }
}
[TestMethod]
public void ThisIsABadTest()
{
var timer = new Timer(1);
var cut = new ClassWithTimer(timer);
Thread.Sleep(100);
Assert.IsTrue(cut.SomethingImportantHappened);
}
very small or zero
next tick/timeout
×Time based == fragile/inconsistent test
× Hard to investigate failures
× Usually comes with Thread.Sleep
Test
Code under Test
Fake
Logic
Assert Results
[TestMethod, Isolated]
public void ThisIsAGoodTest()
{
var fakeTimer = Isolate.Fake.Instance<Timer>();
var cut = new ClassWithTimer(fakeTimer);
var fakeEventArgs = Isolate.Fake.Instance<ElapsedEventArgs>();
Isolate.Invoke.Event(
() => fakeTimer.Elapsed += null, this, fakeEventArgs);
Assert.IsTrue(cut.SomethingImportantHappened);
}
Solution – wrap the unfakeable
× Problem – requires code change
public interface ITimer
{
event EventHandler<EventArgs> Elapsed;
void Start();
void Stop();
}
internal class MyTimer : ITimer
{
private readonly Timer _timer;
public event EventHandler<EventArgs> Elapsed;
public MyTimer(double interval)
{
_timer = new Timer(interval);
_timer.Elapsed += OnTimerElapsed;
}
...
}
[TestMethod]
public void ThisIsAGoodTestWithFakeItEasy()
{
var fakeTimer = A.Fake<ITimer>();
var cut = new ClassWithMyTimer(fakeTimer);
fakeTimer.Elapsed += Raise.With(EventArgs.Empty);
Assert.IsTrue(cut.SomethingImportantHappened);
}
How can we test that an
asynchronous operation
never happens?
Test Code Under Test
Is Test
Async
Deterministic Assert Results
public void Start() {
_cancellationTokenSource = new CancellationTokenSource();
Task.Run(() => {
var message = _messageBus.GetNextMessage();
if(message == null)
return;
// Do work
if (OnNewMessage != null) {
OnNewMessage(this, EventArgs.Empty);
}
}, _cancellationTokenSource.Token);
}
https://helpercode.com/2014/11/23/unit-testing-concurrent-code-using-custom-taskscheduler/
Fake & Sync
Async code  sync test
When you have to run a concurrent test in a predictable way
Synchronized run patterns
Start
Code under test
Run
Fake object
Signal
Call
Wait Assert result
public void DiffcultCalcAsync(int a, int b)
{
Task.Run(() =>
{
Result = a + b;
_otherClass.DoSomething(Result);
});
}
[TestMethod]
public void TestUsingSignal() {
var waitHandle = new ManualResetEventSlim(false);
var fakeOtherClass = A.Fake<IOtherClass>();
A.CallTo(() => fakeOtherClass.DoSomething(A<int>._)).Invokes(waitHandle.Set);
var cut = new ClassWithAsyncOperation(fakeOtherClass);
cut.DiffcultCalcAsync(2, 3);
var wasCalled = waitHandle.Wait(10000);
Assert.IsTrue(wasCalled, "OtherClass.DoSomething was never called");
Assert.AreEqual(5, cut.Result);
}
Start
Code under
test
Run
Assert or
Timeout
× Harder to investigate failures
× Cannot test that a call was not made
Test runs for too long but only when it fails
 Use if other patterns are not applicable
Concurrent unit testing patterns
• Humble object
• Test before – test after
Avoid Concurrency
• Fake & Sync
• Async in production - sync in test
Run in single thread
• The Signaled pattern
• Busy assertion
Synchronize test
Thank you
http://helpercode.com
Demos: https://github.com/dhelper/ConcurrentUnitTesting

Unit testing patterns for concurrent code

  • 1.
  • 2.
    Consultant & softwarearchitect @ Practical Software Developing software since 2002 Clean Coder & Test-Driven Developer Pluralsight author B: http://helpercode.com T: @dhelper About.ME
  • 3.
    The free lunchis over! Multi-core CPUs are the new standard New(er) language constructs New(ish) languages We live in a concurrent world!
  • 4.
    [Test] public void AddTest() { varcut = new Calculator(); var result = cut.Add(2, 3); Assert.AreEqual(5, result); }
  • 6.
    A good unit test mustbe: Trustworthy Maintainable Readable
  • 7.
    smells ×Inconsistent results × Untraceablefail × Long running tests × Test freeze
  • 8.
    public void Start(){ _worker = new Thread(() => { while (_isAlive) { Thread.Sleep(1000); var msg = _messageProvider.GetNextMessage(); //Do stuff LastMessage = msg; } }); _worker.Start(); }
  • 9.
    [TestMethod] public void ArrivingMessagePublishedTest() { varfakeMessageProvider = A.Fake<IMessageProvider>(); A.CallTo(() => fakeMessageProvider.GetNextMessage()).Returns("Hello!"); var server = new Server(fakeMessageProvider); server.Start(); Thread.Sleep(2000); Assert.AreEqual("Hello!", server.LastMessage); }
  • 10.
    Sleep × Time based- fail/pass inconsistently × Test runs for too long × Hard to investigate failures
  • 11.
  • 12.
    [TestMethod] public async TaskArrivingMessagePublishedTest() { var fakeMessageProvider = A.Fake<IMessageProvider>(); A.CallTo(() => fakeMessageProvider.GetNextMessage()).Returns("Hello!"); var server = new Server(fakeMessageProvider); server.Start(); await Task.Delay(2000); Assert.AreEqual("Hello!", server.LastMessage); }
  • 13.
  • 14.
    Code under Test Start Humbleobject Async Perform action Perform Action Assert Result Production Code http://xunitpatterns.com/Humble%20Object.html
  • 15.
    public void Start(){ _worker = new Thread(() => { while (_isAlive) { Thread.Sleep(1000); var msg = _messageProvider.GetNextMessage(); //Do stuff LastMessage = msg; } }); _worker.Start(); }
  • 16.
    public void Start(){ _worker = new Thread(() => { while (_isAlive) { Thread.Sleep(1000); _messageHandler.HandleNextMessage(); } }); _worker.Start(); }
  • 17.
    [TestMethod] public void ArrivingMessagePublishedTest() { varfakeMessageProvider = A.Fake<IMessageProvider>(); A.CallTo(() => fakeMessageProvider.GetNextMessage()).Returns("Hello!"); var messageHandler = new MessageHandler(fakeMessageProvider); messageHandler.HandleNextMessage(); Assert.AreEqual("Hello!", messageHandler.LastMessage); }
  • 18.
    public class MessageManager { privateIMesseageQueue _messeageQueue; public void CreateMessage(string msg) { // Here Be Code! _messeageQueue.Enqueue(message); } } public class MessageClient { private IMesseageQueue _messeageQueue; public string LastMessage { get; set; } private void OnMsg(object o, EventArgs e) { // Here Be Code! LastMessage = e.Message; } }
  • 19.
    Start Code under test Async Logic2 Assertresults Logic1 Start Logic1 Assert ResultsFake Start Logic2 Assert ResultsFake
  • 20.
    [TestMethod] public void AddNewMessageProcessedMessageInQueue() { varmesseageQueue = new AsyncMesseageQueue(); var manager = new MessageManager(messeageQueue); manager.CreateNewMessage("a new message"); Assert.AreEqual(1, messeageQueue.Count); }
  • 21.
    [TestMethod] public void QueueRaisedNewMessageEventClientProcessEvent() { varmesseageQueue = new AsyncMesseageQueue(); var client = new MessageClient(messeageQueue); client.OnMessage(null, new MessageEventArgs("A new message")); Assert.AreEqual("A new message", client.LastMessage); }
  • 22.
    The best possiblesolution No concurrency == no problems × Do not test some of the code × Not applicable in every scenario
  • 23.
    public class ClassWithTimer { privateTimer _timer; public ClassWithTimer(Timer timer) { _timer = timer; _timer.Elapsed += OnTimerElapsed; _timer.Start(); } private void OnTimerElapsed(object sender, ElapsedEventArgs e) { SomethingImportantHappened = true; } public bool SomethingImportantHappened { get; private set; } }
  • 24.
    [TestMethod] public void ThisIsABadTest() { vartimer = new Timer(1); var cut = new ClassWithTimer(timer); Thread.Sleep(100); Assert.IsTrue(cut.SomethingImportantHappened); }
  • 25.
    very small orzero next tick/timeout ×Time based == fragile/inconsistent test × Hard to investigate failures × Usually comes with Thread.Sleep
  • 26.
  • 27.
    [TestMethod, Isolated] public voidThisIsAGoodTest() { var fakeTimer = Isolate.Fake.Instance<Timer>(); var cut = new ClassWithTimer(fakeTimer); var fakeEventArgs = Isolate.Fake.Instance<ElapsedEventArgs>(); Isolate.Invoke.Event( () => fakeTimer.Elapsed += null, this, fakeEventArgs); Assert.IsTrue(cut.SomethingImportantHappened); }
  • 28.
    Solution – wrapthe unfakeable × Problem – requires code change
  • 29.
    public interface ITimer { eventEventHandler<EventArgs> Elapsed; void Start(); void Stop(); }
  • 30.
    internal class MyTimer: ITimer { private readonly Timer _timer; public event EventHandler<EventArgs> Elapsed; public MyTimer(double interval) { _timer = new Timer(interval); _timer.Elapsed += OnTimerElapsed; } ... }
  • 31.
    [TestMethod] public void ThisIsAGoodTestWithFakeItEasy() { varfakeTimer = A.Fake<ITimer>(); var cut = new ClassWithMyTimer(fakeTimer); fakeTimer.Elapsed += Raise.With(EventArgs.Empty); Assert.IsTrue(cut.SomethingImportantHappened); }
  • 32.
    How can wetest that an asynchronous operation never happens?
  • 33.
    Test Code UnderTest Is Test Async Deterministic Assert Results
  • 34.
    public void Start(){ _cancellationTokenSource = new CancellationTokenSource(); Task.Run(() => { var message = _messageBus.GetNextMessage(); if(message == null) return; // Do work if (OnNewMessage != null) { OnNewMessage(this, EventArgs.Empty); } }, _cancellationTokenSource.Token); } https://helpercode.com/2014/11/23/unit-testing-concurrent-code-using-custom-taskscheduler/
  • 36.
    Fake & Sync Asynccode  sync test
  • 37.
    When you haveto run a concurrent test in a predictable way Synchronized run patterns
  • 38.
    Start Code under test Run Fakeobject Signal Call Wait Assert result
  • 39.
    public void DiffcultCalcAsync(inta, int b) { Task.Run(() => { Result = a + b; _otherClass.DoSomething(Result); }); }
  • 40.
    [TestMethod] public void TestUsingSignal(){ var waitHandle = new ManualResetEventSlim(false); var fakeOtherClass = A.Fake<IOtherClass>(); A.CallTo(() => fakeOtherClass.DoSomething(A<int>._)).Invokes(waitHandle.Set); var cut = new ClassWithAsyncOperation(fakeOtherClass); cut.DiffcultCalcAsync(2, 3); var wasCalled = waitHandle.Wait(10000); Assert.IsTrue(wasCalled, "OtherClass.DoSomething was never called"); Assert.AreEqual(5, cut.Result); }
  • 41.
  • 42.
    × Harder toinvestigate failures × Cannot test that a call was not made Test runs for too long but only when it fails  Use if other patterns are not applicable
  • 43.
    Concurrent unit testingpatterns • Humble object • Test before – test after Avoid Concurrency • Fake & Sync • Async in production - sync in test Run in single thread • The Signaled pattern • Busy assertion Synchronize test
  • 44.

Editor's Notes

  • #4  applications will increasingly need to be concurrent if they want to fully exploit CPU throughput gains
  • #7 Trustworthy Consistent results Only fail due to bug or requirement change Maintainable Robust Easy to refactor Simple to update Readable
  • #14 Image by Mark http://upload.wikimedia.org/wikipedia/commons/0/06/Stay_Alive_and_Avoid_Zombies.png
  • #37 Some things cannot be tested