首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >在Jon的"TimeMachine“异步单元测试框架中是否需要TimeMachine?

在Jon的"TimeMachine“异步单元测试框架中是否需要TimeMachine?
EN

Stack Overflow用户
提问于 2017-03-20 08:28:24
回答 2查看 260关注 0票数 7

我刚看过just的一个视频类,他在其中谈到了单元测试异步方法。这是在一个付费网站上,但我在他的中发现了类似他所说的东西(只有Ctrl+F "15.6.3.单元测试异步代码“)。

完整的代码可以在他的集散地上找到,但是为了我的问题,我已经简化了它(我的代码基本上是StockBrokerTest.CalculateNetWorthAsync_AuthenticationFailure_ThrowsDelayed(),但是有TimeMachine和高级程序操作内联)。

让我们假设我们有一个类来测试失败的登录(没有单元测试框架来简化这个问题):

代码语言:javascript
复制
public static class LoginTest
{
    private static TaskCompletionSource<Guid?> loginPromise = new TaskCompletionSource<Guid?>();

    public static void Main()
    {
        Console.WriteLine("== START ==");

        // Set up
        var context = new ManuallyPumpedSynchronizationContext(); // Comment this
        SynchronizationContext.SetSynchronizationContext(context); // Comment this

        // Run method under test
        var result = MethodToBeTested();
        Debug.Assert(!result.IsCompleted, "Result should not have been completed yet.");

        // Advancing time
        Console.WriteLine("Before advance");
        loginPromise.SetResult(null);
        context.PumpAll(); // Comment this
        Console.WriteLine("After advance");

        // Check result
        Debug.Assert(result.IsFaulted, "Result should have been faulted.");
        Debug.Assert(result.Exception.InnerException.GetType() == typeof(ArgumentException), $"The exception should have been of type {nameof(ArgumentException)}.");

        Console.WriteLine("== END ==");
        Console.ReadLine();
    }

    private static async Task<int> MethodToBeTested()
    {
        Console.WriteLine("Before login");
        var userId = await Login();
        Console.WriteLine("After login");
        if (userId == null)
        {
            throw new ArgumentException("Bad username or password");
        }

        return userId.GetHashCode();
    }

    private static Task<Guid?> Login()
    {
        return loginPromise.Task;
    }
}

其中,ManuallyPumpedSynchronizationContext的实现是:

代码语言:javascript
复制
public sealed class ManuallyPumpedSynchronizationContext : SynchronizationContext
{
    private readonly BlockingCollection<Tuple<SendOrPostCallback, object>> callbacks;

    public ManuallyPumpedSynchronizationContext()
    {
        callbacks = new BlockingCollection<Tuple<SendOrPostCallback, object>>();
    }

    public override void Post(SendOrPostCallback callback, object state)
    {
        Console.WriteLine("Post()");
        callbacks.Add(Tuple.Create(callback, state));
    }

    public override void Send(SendOrPostCallback d, object state)
    {
        throw new NotSupportedException("Synchronous operations not supported on ManuallyPumpedSynchronizationContext");
    }

    public void PumpAll()
    {
        Tuple<SendOrPostCallback, object> callback;
        while(callbacks.TryTake(out callback))
        {
            Console.WriteLine("PumpAll()");
            callback.Item1(callback.Item2);
        }
    }
}

产出如下:

代码语言:javascript
复制
== START ==
Before login
Before advance
After login
After advance
== END ==

我的问题是:为什么我们需要ManuallyPumpedSynchronizationContext

为什么默认的SynchronizationContext还不够呢?Post()方法甚至没有被调用(基于输出)。我已经尝试过注释用// Comment this标记的行,输出是相同的,断言是通过的。

如果我正确理解Jon在视频中说的话,那么当我们遇到一个尚未完成的任务时,应该调用SynchronizationContext.Post()方法。但事实并非如此。我遗漏了什么?

辅助信息

通过我的研究,我偶然发现了这个答案。为了尝试它,我将Login()方法的实现更改为:

代码语言:javascript
复制
private static Task<Guid?> Login()
{
    // return loginPromise.Task;
    return Task<Guid?>.Factory.StartNew(
        () =>
        {
            Console.WriteLine("Login()");
            return null;
        },
        CancellationToken.None,
        TaskCreationOptions.None,
        TaskScheduler.FromCurrentSynchronizationContext());
}

通过该修改,确实调用了Post()方法。输出:

代码语言:javascript
复制
== START ==
Before login
Post()
Before advance
PumpAll()
Login()
After login
After advance
== END ==

那么,随着Jon对TaskCompletionSource的使用,他是否不需要创建ManuallyPumpedSynchronizationContext呢?

注意:我认为我看到的视频是在C# 5发布日期前后拍摄的。

EN

回答 2

Stack Overflow用户

发布于 2017-03-20 09:09:40

我不会讨论这段代码的目标是什么,因为我没有读过你发布的github链接上的书或全部代码。我将只处理你在当前问题中发布的代码。

我认为在您提供的代码中,不使用ManuallyPumpedSynchronizationContext (不管您在哪里运行它:控制台应用程序、单元测试、UI应用程序等等)。它的Post方法不会被调用,因为没有同步上下文切换。通常情况下,await的继续将被Post编辑为捕获的同步上下文,这在一般情况下是正确的,但是如果等待的方法完成之后,您仍然处于相同的同步上下文中--没有理由发布任何内容--您位于相同的上下文中,并且可以继续。这就是这里发生的事。当你打电话:

代码语言:javascript
复制
loginPromise.SetResult(null);

当前上下文仍然是ManuallyPumpedSynchronizationContext

但是,如果您像这样更改它:

代码语言:javascript
复制
SynchronizationContext.SetSynchronizationContext(null);
loginPromise.SetResult(null);

现在,当Login()完成时,您不再处于捕获的上下文中,因此继续将确实被Post编辑到它,因此延拓将被延迟到调用PumpAll

更新:请参阅@StephenCleary,以获得更完整的对此行为的解释(在我的回答中还有一个没有提到的因素)。

票数 2
EN

Stack Overflow用户

发布于 2017-03-20 08:37:35

因为您在控制台应用程序中执行代码。

控制台应用程序没有同步上下文,SynchronizationContext.Current将始终为空。

ManuallyPumpedSynchronizationContext的目的是“保存”同步上下文,其中执行测试方法,并将已完成任务的结果“泵”到保存的上下文中。

在控制台应用程序中,保存的上下文是null,所以您没有看到任何区别

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/42898828

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档