.NET Task / TPL测试和嘲笑? (或用法不正确?)

时间:2012-02-28 16:45:55

标签: c# .net unit-testing mocking task-parallel-library

我有一个类,它在远离我的WPF MVVM视图模型的后台线程上抽象执行长时间运行的方法。我也有这个类接口和IoC注入到我的大多数视图模型中。

public interface IAsyncActionManager : INotifyPropertyChanged
{        
    /// <summary>
    /// Sets and gets the IsBusy property.
    /// Changes to that property's value raise the PropertyChanged event. 
    /// </summary>
    bool IsBusy { get; }

    Task StartAsyncTask(Action backgroundAction);
 }

我的视图模型以各种方式使用此类,例如:

private void LoadStuff()
{
    ActionManager.StartAsyncTask(() => { // Load stuff from database here });
}

在我的一些XAML中,我直接绑定到IsBusy属性:

<Grid Cursor="{Binding ActionManager.IsBusy, Converter={Converters:BusyMouseConverter}}">

无论如何 - 现在你有了背景,我现在正试图做一些更有趣的事情:

private Task _saveChangesTask;
public void SaveChanges()
{
    if (_saveChangesTask != null && _saveChangesTask.Status != TaskStatus.Running)
        return;

    _saveChangesTask = ActionManager.StartAsyncTask(() =>
    { 
        // Save stuff here - slowly
    });
}

这是简化的,因为我也通过一个Command对象连接它,WPF在其视图中使用CanExecute等,但这个任务的“缓存”使得保存操作不会运行两次。

现在,遇到问题,我想对这个逻辑进行单元测试 - 我该怎么做? 我已经尝试在我的测试中使用TaskCompletionSource,但是我无法让我的任务进入“运行”状态......?

var tcs = new TaskCompletionSource<object>();
// tcs.Task.Status is now WaitingForActivation

// tcs.Task.Start(Synchronous.TaskScheduler); // Doesn't work - throws an Exception.

A.CallTo(() => mockAsyncActionManager.StartAsyncTask(A<Action>._, A<Action<Task>>._)).Returns(tcs.Task);

有人有任何线索吗?我可以这样做吗?

我知道我错误地使用了TPL - 我不应该依赖任务状态 - 但不确定如何以另一种方式实现类似的事情(建议欢迎)。

干杯,

1 个答案:

答案 0 :(得分:2)

我相信这里的问题确实存在(正如你所说的)Status property的检查。

TaskStatus enumeration表示Task实例不仅具有运行/未运行的二进制状态,而是可以处于多个状态。

创建Task后,根据TaskSchedulerTask会将Running置于 Created州之前的以下状态

  • WaitingForActivation - 任务已初始化但尚未安排。
  • WaitingToRun - 任务正在等待由.NET Framework基础架构在内部激活和调度。
  • TaskStatus - 任务已安排执行但尚未开始执行。

这样,您对Running Task的检查可能会失败,因为它处于以上某种状态。

我建议您只是检查null的参考;如果是Task,则创建一个新的SaveChanges,否则只返回。

这里的假设是对Task的调用意味着在对象上调用一次(或者在保存完成之前不做任何事情)。

如果您打算再次调用该方法(可能是因为已经进行了其他更改),您应该在Task上继续设置null到{{{{}}的引用1}}操作完成时。这样,当第二次调用SaveChanges时,对引用的检查将成功。

旁注,I've pointed out in the comments that you have a race condition。如果您要在继续中将Task的引用设置回null,那么您将需要以线程安全的方式执行检查和分配(因为继续将运行在另一个线程上),像这样:

private Task _saveChangesTask;

// Used to synchronize access to _saveChangesTask
private readonly object _saveChangesTaskLock = new object();

public void SaveChanges()
{
    // Guard access to the reference.
    lock (_saveChangesTaskLock)
    {
        // Check and assign.
       if (_saveChangesTask != null) return;

        _saveChangesTask = ActionManager.StartAsyncTask(() =>
        { 
            // Save stuff here - slowly

            // Done saving stuff here - slowly
            // (BTW, is the above a reference from "True Lies"?)
            // Remove reference to task.  This is on another thread
            // so using a lock again is ok.
            // Guard access to the reference.
            lock (_saveChangesTaskLock) _saveChangesTask = null;
        });
    }
}