Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save ritalin/2880759 to your computer and use it in GitHub Desktop.
Save ritalin/2880759 to your computer and use it in GitHub Desktop.
Unit Testing, Part II, Synchronisation Issues
public class ComplexBusinessOperations
{
private int moneyEarnedSoFar;
public int MoneyEarnedSoFar
{
get { return moneyEarnedSoFar; }
}
public void EarnMoney(int investment)
{
EarnMoneySubTaks(investment);
EarnMoneySubTaks(investment);
}
private void EarnMoneySubTaks(int investment)
{
for (int i = 0; i < investment; i++)
{
var result = EarnOneDollar();
moneyEarnedSoFar += result;
}
}
private int EarnOneDollar()
{
// This operation takes a while
Thread.Sleep(50);
return 1;
}
}
public class ComplexBusinessOperations
{
private int moneyEarnedSoFar;
public int MoneyEarnedSoFar
{
get { return moneyEarnedSoFar; }
}
public async Task EarnMoneyAsync(int investment)
{
var firstMoneyEarner = EarnMoneySubTaskAsync(investment);
var secondMoneyEarner = EarnMoneySubTaskAsync(investment);
await TaskEx.WhenAll(firstMoneyEarner, secondMoneyEarner);
}
private async Task EarnMoneySubTaskAsync(int investment)
{
for (int i = 0; i < investment; i++)
{
var result = await EarnOneDollarAsync();
moneyEarnedSoFar += result;
}
}
private Task<int> EarnOneDollarAsync()
{
return TaskEx.Run(
() =>
{
// This operation takes a while
Thread.Sleep(50);
return 1;
});
}
}
[Test]
public void EarnMoney()
{
var toTest = new ComplexBusinessOperations();
toTest.EarnMoney(200);
Assert.AreEqual(400, toTest.MoneyEarnedSoFar);
}
[Test]
public void EarnMoney()
{
var toTest = new ComplexBusinessOperations();
var task = toTest.EarnMoneyAsync(200);
task.Wait();
Assert.AreEqual(400, toTest.MoneyEarnedSoFar);
}
[Test]
public void EarnMoney()
{
TestSyncContext.Run(
awaiter=>
{
var toTest = new ComplexBusinessOperations();
var task = toTest.EarnMoneyAsync(200);
awaiter.WaitFor(task);
Assert.AreEqual(400, toTest.MoneyEarnedSoFar);
});
}
class MyPrimitiveSynchronisationContext : SynchronizationContext, IDisposable
{
private readonly Queue<Action> messagesToProcess = new Queue<Action>();
private readonly object syncHandle = new object();
private bool isRunning = true;
public override void Send(SendOrPostCallback codeToRun, object state)
{
throw new NotImplementedException();
}
public override void Post(SendOrPostCallback codeToRun, object state)
{
lock (syncHandle)
{
if(!isRunning)
{
throw new InvalidOperationException("Cannot post messages to a closes message pump");
}
messagesToProcess.Enqueue(() => codeToRun(state));
SignalContinue();
}
}
public void RunMessagePump()
{
while (CanContinue())
{
RunOneRound();
}
}
public void RunOneRound()
{
Action nextToRun = GrabItem();
nextToRun();
}
public void Dispose()
{
lock (syncHandle)
{
isRunning = false;
SignalContinue();
}
}
private Action GrabItem()
{
lock (syncHandle)
{
while (CanContinue() && messagesToProcess.Count == 0)
{
Monitor.Wait(syncHandle);
}
return messagesToProcess.Dequeue();
}
}
private bool CanContinue()
{
lock (syncHandle)
{
return isRunning;
}
}
private void SignalContinue()
{
Monitor.Pulse(syncHandle);
}
}
public static class TestSyncContext
{
public static void Run(Action<Awaiter> testToRun)
{
using(var msgPump = new MyPrimitiveSynchronisationContext())
{
var syncContextBackup = SynchronizationContext.Current;
try
{
SynchronizationContext.SetSynchronizationContext(msgPump);
var awaiter = new Awaiter(msgPump);
msgPump.Post(obj => testToRun(awaiter), null);
msgPump.RunOneRound();
}
finally
{
SynchronizationContext.SetSynchronizationContext(syncContextBackup);
}
}
}
}
public class Awaiter
{
private readonly MyPrimitiveSynchronisationContext syncContext;
internal Awaiter(MyPrimitiveSynchronisationContext syncContext)
{
this.syncContext = syncContext;
}
public void WaitFor(IAsyncResult toWaitOn)
{
while(!toWaitOn.IsCompleted)
{
syncContext.RunOneRound();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment