Autotests for Task.ContinueWIth logic
I'm trying to cover some logic with unit tests and have a problem with Task.ContinueWith
method. The problem is that I have an important logic in the ContinueWith
task. The ContinueWith
task will be executed after the specified task, but there is no guarantee it will be executed immediately. So as a result, my test sometimes fails and sometimes succeeds.
There is my code:
The method:
public IPromise CreateFromTask(Task task)
{
var promise = new ControllablePromise(_unityExecutor);
task.ContinueWith(t =>
{
if (t.IsCanceled)
Debug.LogWarning("Promises doesn't support task canceling");
if (t.Exception == null)
promise.Success();
else
{
Debug.Log(t.Exception.InnerException);
promise.Fail(t.Exception.InnerException);
}
}, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.PreferFairness);
return promise;
}
The test:
private PromiseFactory CreateFactory(out IUnityExecutor unityExecutor)
{
unityExecutor = Substitute.For<IUnityExecutor>();
return new PromiseFactory(unityExecutor);
}
[Test]
public void CreateFromTask_FailedTask_OnFailExecuted()
{
// Arrange
var factory = CreateFactory(out var unityExecutor);
var testException = new Exception("Test exception");
void TaskAction()
{
throw testException;
}
Exception failException = null;
void FailCallback(Exception exception)
{
failException = exception;
}
unityExecutor.ExecuteOnFixedUpdate(Arg.Do<Action>(x => x?.Invoke()));
// Act
Debug.Log("About to run a task");
var task = Task.Run(TaskAction);
Debug.Log("The task run");
var promise = factory.CreateFromTask(task);
Debug.Log("Promise created");
Task.WaitAny(task.ContinueWith(t => { },
TaskContinuationOptions.PreferFairness | TaskContinuationOptions.ExecuteSynchronously));
Debug.Log("Task awaited");
promise.OnFail(FailCallback);
// Assert
Assert.AreEqual(testException, failException);
}
Log output is next:
As you can see, I've already tried to use TaskContinuationOptions.ExecuteSynchronously
and TaskContinuationOptions.PreferFairness
to fix this, but it didn't help. I was very surprised that even with these options, my test didn't work.
If it is important I'm doing all of this in Unity3d with its standard test framework.
Expected result that the error always should be logged before "Task awaited" log.
Solution 1:
I'd rather somehow wait for the continuation task, which is now abandoned inside the CreateFromTask method. But, if it's impossible:
1. If the Promise supports multiple subscriptions, you can create an extension method in your Unit Tests project as shown below:
public static class PromiseExtensions
{
public static Task AsTask(this IPromise promise)
{
var tcs = new TaskCompletionSource();
promise.OnFail(exception => { tcs.TrySetResult(); });
promise.OnSuccess(() => { tcs.TrySetResult(); });
return tcs.Task;
}
}
[Test]
public void CreateFromTask_FailedTask_OnFailExecuted()
{
// Arrange
var factory = CreateFactory(out var unityExecutor);
var testException = new Exception("Test exception");
void TaskAction()
{
throw testException;
}
Exception failException = null;
void FailCallback(Exception exception)
{
failException = exception;
}
unityExecutor.ExecuteOnFixedUpdate(Arg.Do<Action>(x => x?.Invoke()));
// Act
var task = Task.Run(TaskAction);
var promise = factory.CreateFromTask(task);
Task.WaitAny(promise.AsTask(), Task.Delay(TimeSpan.FromSeconds(1)));
promise.OnFail(FailCallback);
// Assert
Assert.AreEqual(testException, failException);
}
2. If the Promise doesn't support multiple subscriptions, just wait until the FailCallback is executed:
[Test]
public void CreateFromTask_FailedTask_OnFailExecuted()
{
// Arrange
var factory = CreateFactory(out var unityExecutor);
var testException = new Exception("Test exception");
void TaskAction()
{
throw testException;
}
TaskCompletionSource failCallbackCompletionSource = new TaskCompletionSource();
Exception failException = null;
void FailCallback(Exception exception)
{
failException = exception;
failCallbackCompletionSource.TrySetResult();
}
unityExecutor.ExecuteOnFixedUpdate(Arg.Do<Action>(x => x?.Invoke()));
// Act
var task = Task.Run(TaskAction);
var promise = factory.CreateFromTask(task);
promise.OnFail(FailCallback);
Task.WaitAny(failCallbackCompletionSource.Task, Task.Delay(TimeSpan.FromSeconds(1)));
// Assert
Assert.AreEqual(testException, failException);
}