【问题标题】:How can I unit test a method that spins off a Task.ContinueWith?如何对衍生 Task.ContinueWith 的方法进行单元测试?
【发布时间】:2015-12-14 16:50:50
【问题描述】:

考虑以下代码:

public interface IBar
{
    Task<IEnumerable<string>> GetStringsAsync();
}

public class Foo
{
    public Foo(IBar bar, IList<string> initial)
    {
        MyCollection = new ObservableCollection<string>();
        if (initial == null || !initial.Any())
            AddContent(bar);
        else
            MyCollection.AddRange(initial);
    }

    public ObservableCollection<string> MyCollection { get; private set; }

    public void AddContent(IBar bar)
    {
        var cancel = new CancellationTokenSource();
        bar.GetStringsAsync().ContinueWith(
            task => MyCollection.AddRange(task.Result),
            cancel,
            TaskContinuationOptions.NotOnCancel,
            TaskScheduler.FromCurrentSynchronizationContext());
    }
}

如何对 Foo.AddContent 方法进行单元测试?我想测试我的模拟 IBar 提供的字符串是否确实被添加到集合中,但断言总是在任务更新集合之前被调用。

我正在使用 .NET 4.5.2。我的第一选择是在 AddContent 中使用 asyncawait,但是因为在构造函数中使用了该方法,所以我认为最好避免这种情况。我需要一些可以启动数据异步加载但不会等待它完成的东西。

欢迎提出有关如何重写 AddContent 的建议,但我已经尝试了很多东西,这是唯一一个效果很好的,所以我真正想要的是一种测试它的方法。

【问题讨论】:

    标签: c# unit-testing asynchronous synchronizationcontext


    【解决方案1】:

    更新 2

    使用 Stephen Cleary 在此处找到的异步初始化模式。

    http://blog.stephencleary.com/2013/01/async-oop-2-constructors.html

    更新

    由于问题已经改变,现在的要求是让构造函数接受变量IBar。根据传递给构造函数的IBar 变量的硬性要求,我建议以下内容:

    public class Foo
    {
        public Foo(IBar bar)
        {
            MyCollection = new ObservableCollection<string>();
            MyCollection.AddRange(bar.GetStringsAsync().Result));
        }
    
        public ObservableCollection<string> MyCollection { get; private set; }
    
        public async Task AddContent(IBar bar)
        {
            MyCollection.AddRange(await bar.GetStringsAsync());
        }
    }
    

    注意:public 方法仍然使用首选机制async / await,但构造函数只是调用.Result。这是构造函数中的阻塞调用,是一种非常糟糕的做法。这很容易被认为是你永远不应该做的事情......

    我强烈建议您的构造函数采用初始字符串,而不是这样,(特别是考虑到它仅用于它返回的字符串!):

    public class Foo
    {
        public Foo(IEnumerable<string> strings)
        {
            MyCollection = new ObservableCollection<string>();
            MyCollection.AddRange(strings);
        }
    
        public ObservableCollection<string> MyCollection { get; private set; }
    
        public async Task AddContent(IBar bar)
        {
            MyCollection.AddRange(await bar.GetStringsAsync());
        }
    }
    

    用法

    [TestMethod]
    public async Task Test()
    {
        IBar bar = GetMockedBarImpl();
        var sut = new Foo(await bar.GetStringsAsync());        
    
        Assert.IsTrue(sut.MyCollection.Any());
        // TODO: Add asserts for known strings in collection...
    }
    

    【讨论】:

    • 这不是大致相当于var strings = await bar.GetStringsAsync(); cancel.ThrowIfCancellationRequested(); MyCollection.AddRange(strings);吗?
    • 我认为这是一个很远的步骤 - 你刚刚删除了取消逻辑
    • 反正也没被使用。
    • 我已经更新了我的问题。我更喜欢使用async/await,但是该方法需要可以从构造函数中调用。
    • 您可能会在此处找到所需的内容:blog.stephencleary.com/2013/01/async-oop-2-constructors.html。这是 Stephen Cleary 关于构造函数中的异步代码的文章。我仍然不明白你为什么需要这样做,对我来说应该避免这样做。
    【解决方案2】:

    看起来有问题的代码存在继承竞争条件。我会等待GetStringAsync 完成并分配给MyCollection

    public void AddContent(IBar bar)
    {
        var cancel = new CancellationTokenSource();
        var result = bar.GetStringsAsync().ContinueWith(
            task => task.Result,
            cancel,
            TaskContinuationOptions.NotOnCancel,
            TaskScheduler.FromCurrentSynchronizationContext());
    
       MyCollection.AddRange(result.Result);
    }
    

    或者干脆

    public void AddContent(IBar bar)
    {
        var result = bar.GetStringsAsync().Result;
        MyCollection.AddRange(result);
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-07-29
      • 1970-01-01
      • 1970-01-01
      • 2011-10-18
      • 2017-07-12
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多