.NET Task / TPL тестирование и макет? (или неправильное использование?) - PullRequest
2 голосов
/ 28 февраля 2012

У меня есть класс, который абстрагирует выполнение длительных методов в фоновом потоке от моих представлений 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 Ответ

2 голосов
/ 28 февраля 2012

Я полагаю, что проблема здесь (как вы сказали) связана с проверкой свойства Status .

Перечисление TaskStatus указывает на то, что экземпляр Task не просто имеет двоичное состояние запуска / не запуска, а может находиться в ряде состояния.

Когда создается Task, в зависимости от TaskScheduler, он переводит Task в следующие состояния до состояние Running:

  • Created - Задача была инициализирована, но еще не была запланирована.
  • WaitingForActivation - Задача ожидает своей активации и планирования внутри инфраструктуры .NET Framework.
  • WaitingToRun - Задание было запланировано для выполнения, но еще не началось.

При этом ваша проверка на TaskStatus из Running может не пройти, поскольку она находится в одном из указанных выше состояний.

Я бы порекомендовал вам просто проверить ссылку на Task; если это null, то создайте новый Task, в противном случае просто верните.

Здесь предполагается, что вызов SaveChanges должен вызываться один раз для объекта (или не делать ничего, пока сохранение не будет завершено).

Если вы собираетесь снова вызвать метод (предположительно, потому что были сделаны другие изменения), у вас должно быть продолжение на Task, которое установит ссылку на Task на null при выполнении операции завершено. Таким образом, проверка по ссылке будет успешной, когда SaveChanges вызывается во второй раз.

На дополнительном примечании Я указал в комментариях, что у вас есть состояние гонки Если вы собираетесь установить ссылку на 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;
        });
    }
}
...