I am trying to test some classes that rely on a Task to do some background computation (retrieve data from a network location). The class gets a non-started instance of a Task, adds a ContinueWith method and then calls Start on the Task. Something like this:
private void Search()
{
Task<SearchResults> searchTask = m_searchProvider.GetSearchTask(m_searchCriteria);
searchTask.ContinueWith(OnSearchTaskCompleted);
searchTask.Start();
}
The class gets hold of the instance of the Task through an interface, so I am able to inject a Task instance that my test is in control of. I can't seem to create one that I have enough control over, however.
I don't want to introduce threading into the test, but still want to make the Task behave asynchronously, so what I have tried to do is write a class that implements the BeginInvoke/EndInvoke pattern without threading, and use the TaskFactory.FromAsync method to create the Task.
The idea being that the test can invoke the method on the class that starts the task, and then when that returns the test can supply the result data to the Async object, which finishes the operation whilst remaining on the same thread.
However, when I try and call Start on that Task, I get an error stating that "Start may not be called on a task with null action." Google doesn't much help me on that message, unfortunately, so I'm unsure whether I've implemented my Async object incorrectly, or am using the TaskFactory.FromAsync wrongly. Here's my code for my NonThreadedAsync classes and a test that blows up with the exception:
public class NonThreadedAsync<TResult>
{
private NonThreadedAsyncResult<TResult> m_asyncResult;
public IAsyncResult BeginInvoke(
AsyncCallback callback,
object state)
{
m_asyncResult = new NonThreadedAsyncResult<TResult>(callback, state);
return m_asyncResult;
}
public TResult EndInvoke(IAsyncResult asyncResult)
{
return m_asyncResult.GetResults();
}
public void Complete(TResult data)
{
m_asyncResult.CompleteAsync(data);
}
}
public class NonThreadedAsyncResult<TResult> : IAsyncResult
{
private readonly AsyncCallback m_asyncCallback;
private readonly object m_state;
private readonly ManualResetEvent m_waitHandle;
private bool m_isCompleted;
private TResult m_resultData;
public NonThreadedAsyncResult(AsyncCallback asyncCallback, object state)
{
m_asyncCallback = asyncCallback;
m_state = state;
m_waitHandle = new ManualResetEvent(false);
m_isCompleted = false;
}
public void CompleteAsync(TResult data)
{
m_resultData = data;
m_isCompleted = true;
m_waitHandle.Set();
if (m_asyncCallback != null)
{
m_asyncCallback(this);
}
}
public TResult GetResults()
{
if (!m_isCompleted)
{
m_waitHandle.WaitOne();
}
return m_resultData;
}
#region Implementation of IAsyncResult
public bool IsCompleted
{
get { return m_isCompleted; }
}
public WaitHandle AsyncWaitHandle
{
get { return m_waitHandle; }
}
public object AsyncState
{
get { return m_state; }
}
public bool CompletedSynchronously
{
get { return false; }
}
#endregion
}
[TestClass]
public class NonThreadedAsyncTests
{
[TestMethod]
public void TaskFactoryFromAsync_CanStartReturnedTask()
{
NonThreadedAsync<int> async = new NonThre开发者_如何学JAVAadedAsync<int>();
Task<int> task = Task<int>.Factory.FromAsync(async.BeginInvoke, async.EndInvoke, null);
task.Start();
}
}
As further information, if I debug that test, just before it calls Start(), the task instance is showing up in the Locals window like this:
Id = 1, Status = WaitingForActivation, Method = "{null}", Result = "{Not yet computed}"
but there is no Method property in the visible properties if I expand it.
Can anyone see what I'm doing wrong?
[Edit: I've also written a test that confirms the NonThreadedAsync class works correctly with the classic Begin/End pattern (or at least, my understanding of the Begin/End pattern :)) and this passes:
[TestMethod]
public void NonThreadedAsync_ClassicAccessPattern()
{
int result = 0;
bool asyncCompleted = false;
NonThreadedAsync<int> async = new NonThreadedAsync<int>();
async.BeginInvoke(asyncResult =>
{
result = async.EndInvoke(asyncResult);
asyncCompleted = true;
},
null);
Assert.IsFalse(asyncCompleted);
Assert.AreEqual(0, result);
async.Complete(54);
Assert.IsTrue(asyncCompleted);
Assert.AreEqual(54, result);
}
Oh, I get it. Our API was wrong in that it was trying to return non-started Tasks. Removing the Start() from inside the class under test solves the issue. In researching this, however, I have also found that I was doing far too much to get a test-controlled async Task. As per Stephen Toub's post here, we can simply use a TaskCompletionSource:
[TestMethod]
public void TaskCompletionSource_WorksALotBetterThanMyOverEngineeredCustomStuff()
{
int result = 0;
TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
Task<int> myTask = tcs.Task;
// Pretend this is the class under test and that we've
// passed in myTask
myTask.ContinueWith(t => { result = t.Result; },
TaskContinuationOptions.ExecuteSynchronously);
Assert.AreEqual(0, result);
tcs.SetResult(54);
Assert.AreEqual(54, result);
}
精彩评论