当 ContinueWith 与 System.Threading.Tasks.Task 一起使用时,单元测试失败

Unit test is failing when ContinueWith is used with System.Threading.Tasks.Task

我正在尝试为我的代码添加单元测试,我正在使用来自 TPL 的 Task 将值更新到数据库中。对于单元测试,我使用 NUnitMoq。这是我项目中的一些代码片段。

*//service*
public interface IServiceFacade{
   Task SynchronizeDataset (string datasetName);
}

*//The method call I want to test*
_ServiceFacade.SynchronizeDataset(DATASET_NAME);

*//In my test, I want to verify if this method is called*
mock_IServicesFacade.Setup(sf => sf.SynchronizeDataset(It.IsAny<string>())).Returns(It.IsAny<Task>());
presenter.InitializeView();
mock_IServicesFacade.Verify(sf => sf.SynchronizeDataset(NSUserUtilStrings.DATASET_ACHIEVEMENT), Times.Once());

这是有效的。但是当我用这样的服务方法调用添加 ContinueWith 时...

_ServiceFacade.SynchronizeDataset(DATASET_NAME).ContinueWith(t =>
{
    if (t.IsFaulted)
    {
        //do something
    }
});

此测试代码不是 working.Test 失败并显示此错误...

System.NullReferenceException : Object reference not set to an instance of an object

堆栈跟踪:

atPresenters.UnitTests.DeviceCategoryPresenterTest.InitializeView_Called () [0x00241] in DeviceCategoryPresenterTest.cs:56 at (wrapper managed-to-native) System.Reflection.MonoMethod:InternalInvoke (System.Reflection.MonoMethod,object,object[],System.Exception&) at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00038] in /private/tmp/source-mono-4.8.0/bockbuild-mono-4.8.0-branch/profiles/mono-mac-xamarin/build-root/mono-x86/mcs/class/corlib/System.Reflection/MonoMethod.cs:305

而且我不确定该如何解决。请帮忙。提前致谢。

在您的设置中,您将函数设置为 return null。您已经在评论中说明了这一点,It.IsAny<Task>() returns null.

Setup(sf => sf.SynchronizeDataset(It.IsAny<string>()))
    .Returns(It.IsAny<Task>());

因此,如果我们将其分解:

_ServiceFacade.SynchronizeDataset(DATASET_NAME).ContinueWith(t =>
           {
               if (t.IsFaulted)
               {
                  //do something
               }
           });

...等于

// This works, but returns null, so testing anything from this point is limited.
var myNullTask = _ServiceFacade.SynchronizeDataset(DATASET_NAME);

myNullTask.ContinueWith(t => ... ); // This yields NullReferenceException
((Task)null).ContinueWith(t => ... ); // Equivalent to line above

似乎您正在编写不适用于您的代码的集成测试(如果您的实际代码确实假定 non-null 为 return)。如果是这种情况,我建议将您的设置更改为:

Setup(sf => sf.SynchronizeDataset(It.IsAny<string>()))
    .Returns(Task.CompletedTask);

这里的事实是,您通过传递有效任务而不是 It.IsAny<Task> 来跳过继续。一个例子是做这样的事情

.NET < v4.6

mock_IServicesFacade
    .Setup(sf => sf.SynchronizeDataset(It.IsAny<string>()))
    .Returns(Task.FromResult(true)))

.NET >= v4.6

mock_IServicesFacade
    .Setup(sf => sf.SynchronizeDataset(It.IsAny<string>()))
    .Returns(Task.CompletedTask))

您甚至可以尝试使用选项 TaskContinuationOptions.OnlyOnFaulted 继续您的工作,因为您只对 IsFaulted 场景感兴趣。

请注意,您不是在测试延续部分,只是跳过它。如果你真的想要 test\verify 延续部分,请小心。看来您的逻辑是服务端逻辑,因此 TaskScheduler 将使用默认 SynchronizationContext 并在 ThreadPool 线程上安排继续。当然,这是在相同的单元测试运行器上下文中执行的。基本上你的测试甚至可以在继续任务执行之前完成。