Implementations of asynchronous pattern can differ, in this entry I decided to share my observations on design features required to make it more testable. Lets take a sample (please note that synchronization on the param is omitted to simplify the scope and real implementation would have to volatile/synchronize):
public class AsyncBench
{
private int param;
internal int Param { get { return param; } }
public AsyncBench(int param)
{
this.param = param;
}
public void BeginMethod()
{
Func<int, int> method = Method;
IAsyncResult ar = method.BeginInvoke(Param, AsyncMethodCallback, new State {Field = 10});
}
private int Method(int param)
{
return param + 1;
}
public void AsyncMethodCallback(IAsyncResult ar)
{
var asyncResult = ar as AsyncResult;
try
{
param = (asyncResult.AsyncDelegate as Func<int, int>).EndInvoke(ar);
}
catch (Exception)
{
throw;
}
}
}
public class State { public int Field { get; set; } }
While this implementation can be totally suitable for some contexts, it lacks some important points from the testability perspective.
We want to be able to verify that Method is called and that Callback is setting the result right.
We also want to be able to wait for the Method to be executed. The solution I found for this is to adjust the implementation towards the Strategy pattern and follow more the pattern of Begin[Method], End[Method]. The current sample is based onto callback (End[Method] would represent a slight variation).
The amended code looks like:
public class AsyncBench
{
private int param;
internal int Param { get { return param;}}
internal readonly Func<int, int> method;
public AsyncBench(int param)
{
this.param = param;
this.method = Method;
}
public AsyncBench(int param, Func<int, int> method) : this(param)
{
this.method = method;
}
public IAsyncResult BeginMethod()
{
return method.BeginInvoke(param, AsyncMethodCallback, new State {Field = param});
}
private int Method(int n)
{
return n + 1;
}
public void AsyncMethodCallback(IAsyncResult ar)
{
var asyncResult = ar as AsyncResult;
try
{
param = (asyncResult.AsyncDelegate as Func<int, int>).EndInvoke(ar);
}
catch (Exception)
{
throw;
}
}
}
public class State { public int Field { get; set; } }
Our test then consists of two methods. One to verify that method is called:
[TestMethod]
public void BeginMethodTest()
{
bool methodCalled = false;
// setup the method
Func<int, int> method = (n) =>
{
methodCalled = true;
return -1;
};
// setup the test instance
var asyncSample = new AsyncBench(
20, method);
IAsyncResult ar = asyncSample.BeginMethod();
// wait until method call completes
ar.AsyncWaitHandle.WaitOne();
// verify it is completed
Assert.IsTrue(ar.IsCompleted, "Operation should have completed before reaching this point!");
// verify our delegate was called
Assert.IsTrue(methodCalled, "Test method should have been called, but it was not!");
// The bellow assert would require more synchronization and exceeds the testing contract
//Assert.AreEqual(-1, asyncSample.Param);
}
And another one to see that callback is doing its job:
[TestMethod]
public void AsyncMethodCallbackTest()
{
bool methodCalled = false;
// setup method
Func<int, int> method = (n) =>
{
methodCalled = true;
return -1;
};
// setup test instance
var asyncSample = new AsyncBench(
20, method);
IAsyncResult ar = method.BeginInvoke(20, null, new State {Field = 10});
// wait for the method to complete
ar.AsyncWaitHandle.WaitOne();
// verify it has completed
Assert.IsTrue(ar.IsCompleted, "Operation should have completed before reaching this point!");
// and was really called
Assert.IsTrue(methodCalled, "Test method should have been called, but it was not!");
// use IAsyncResult from our own BeginInvoke for the callback on the test instance
asyncSample.AsyncMethodCallback(ar);
// check that EndInvoke worked as expected
Assert.AreEqual(-1, asyncSample.Param);
}
Examples provided are very simplistic and just point into common ways I've been pushed to when testing my async methods.
Also in some places I used public or internal accessibility where private could/should have been used, but I just wanted to avoid usage of private accessors for those short samples.
No comments:
Post a Comment