Task.ContinueWIth 逻辑的自动测试

Autotests for Task.ContinueWIth logic

我试图用单元测试来覆盖一些逻辑,但 Task.ContinueWith 方法有问题。问题是我在 ContinueWith 任务中有一个重要的逻辑。 ContinueWith 任务将在指定任务之后执行,但不保证它会立即执行。所以结果,我的测试时而失败时而成功。

有我的代码:

方法:

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;
}

测试:


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);
}

接下来是日志输出:

如您所见,我已经尝试使用 TaskContinuationOptions.ExecuteSynchronouslyTaskContinuationOptions.PreferFairness 来解决此问题,但没有帮助。我很惊讶,即使有这些选项,我的测试也没有成功。

如果这很重要,我将使用其标准测试框架在 Unity3d 中完成所有这些工作。

错误总是应该在“等待任务”日志之前记录的预期结果。

您不需要再创建一个ContinueWith任务,您只需要等待之前创建的任务即可。

public IPromise CreateFromTask(ref Task task)
{
    var promise = new ControllablePromise(_unityExecutor);

    /* Replace the task with the new one */
    task = 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);
        }
    });
    return promise;
}

// Act
Debug.Log("About to run a task");
var task = Task.Run(TaskAction);
Debug.Log("The task run");
var promise = factory.CreateFromTask(ref task);
Debug.Log("Promise created");
Task.WaitAny(task); /* Waiting for the task */
Debug.Log("Task awaited");
promise.OnFail(FailCallback);

如果要等待多任务

// Act
Debug.Log("About to run a task");
var task = Task.Run(TaskAction);
var task2 = Task.Run(TaskAction);
Debug.Log("The task run");
var promise = factory.CreateFromTask(ref task);
Debug.Log("Promise created");
Task.WaitAll(task, task2.ContinueWith(t => { })); /* Waiting for all tasks */
Debug.Log("Task awaited");
promise.OnFail(FailCallback);

随机猜测,在 Mono Behaviors 中使用 ContinueWith 和 Unity 时,将其保持在主线程中很重要。您可以使用以下代码段。在 firebase article.

中有更好的描述
var taskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); 
task.ContinueWith(t =>  { // perform work here }, taskScheduler); 

我宁愿以某种方式等待继续任务,它现在在 CreateFromTask 方法中被放弃了。但是,如果这是不可能的:

1.如果Promise支持多个订阅,你可以在你的单元测试项目中创建一个扩展方法,如下所示:

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.如果Promise不支持多订阅,等FailCallback执行完即可:

[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);
}