Модульное тестирование кода, использующего Task.Фабрика.StartNew().ContinueWith()


Итак, у меня есть некоторый код

Task.Factory.StartNew(() => this.listener.Start()).ContinueWith(
                    (task) =>
                        {
                            if (task.IsCompleted)
                            {
                                this.status = WorkerStatus.Started;
                                this.RaiseStatusChanged();
                                this.LogInformationMessage("Worker Started.");
                            }
                        });

Когда я тестирую, я издеваюсь над всеми зависимыми объектами (Намли это.слушатель.Начать()). проблема в том, что тест завершает выполнение до того, как может быть вызван ContinueWith. Когда я отлаживаю его, он вызывается нормально из-за дополнительной задержки при прохождении кода.

Итак, как я могу-из тестового кода в другой сборке-гарантировать, что код будет запущен до того, как мой тест достигнет своих утверждений?

Я мог бы просто использовать поток.Спать... но это кажется как будто это действительно банальный способ сделать это.

Я думаю, что ищу версию задачи потока.Присоединяйтесь.

5 13

5 ответов:

Рассмотрим следующее:

public class SomeClass
{
    public void Foo()
    {
        var a = new Random().Next();
    }
}

public class MyUnitTest
{
    public void MyTestMethod()
    {
        var target = new SomeClass();        
        target.Foo(); // What to assert, what is the result?..
    }
}

Какое значение присваивается a? Вы не можете сказать, если результат не возвращается вне метода Foo() (как возвращаемое значение, публичное свойство, событие и т. д.).

Процесс "координации действий потоков для получения предсказуемого результата " называется синхронизация.

Одним из самых простых решений в вашем случае может быть возврат экземпляра Task класс и использование его Wait() Метод:

var task = Task.Factory.StartNew(() => Method1())
    .ContinueWith(() => Method2());

Нет необходимости ждать первой задачи, потому что ContinueWith () создает продолжение, которое выполняется асинхронно , когда целевая задача завершается (MSDN):

task.Wait();

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

Единственное другое решение, которое я рассматривал, - это скрытие задачи.Фабрика.Вызов StartNew () за интерфейсом, который вы можете имитировать из вашего теста, таким образом удаляя фактическое выполнение задачи полностью в тестовом сценарии (но все еще есть ожидание, что метод интерфейса будет называемый. Например:
public interface ITaskWrapper
{
    void TaskMethod();
}

И ваша конкретная реализация:

public class MyTask : ITaskWrapper
{
    public void TaskMethod()
    {
        Task.Factory.StartNew(() => DoSomeWork());
    }
}

Затем просто имитируйте ITaskWrapper в своем тестовом методе и установите ожидание вызова TaskMethod.

Если есть какой-либо способ уведомить вас о завершении обработки (можете ли вы добавить обработчик для этого события StatusChanged?), используйте ManualResetEvent и ждите его с разумным таймаутом. Если тайм-аут истек, провалите тест, в противном случае продолжайте выполнять свои утверждения.

Например

var waitHandle = new ManualResetEvent(false);
sut.StatusChanged += (s, e) => waitHandle.Set();

sut.DoStuff();

Assert.IsTrue(waitHandle.WaitOne(someTimeout), "timeout expired");
// do asserts here

Задача продолжения будет выполняться независимо от того, была ли начальная задача выполнена до вызова ContinueWith() или нет. Я дважды проверил это следующим образом:

// Task immediately exits
var task = Task.Factory.StartNew(() => { });

Thread.Sleep(100);

// Continuation on already-completed task
task.ContinueWith(t => { MessageBox.Show("!"); });

Отлаживайте дальше. Может быть, ваша задача провалилась.

При работе с асинхронными процессами в тестируемом коде, использующими реактивные расширения, одним из подходов является использование TestScheduler. TestScheduler может быть перемещен вперед во времени, освобожден от всех запланированных задач и т. д. Таким образом, тестируемый код может принимать IScheduler, для которого вы предоставляете экземпляр TestScheduler. Тогда ваш тест может управлять временем без необходимости на самом деле спать, ждать или синхронизировать. Усовершенствованием этого подхода является ISchedulerProvider ли Кэмпбелла подход.

Если вы используете Observable.Начните вместо задачи.Фабрика.StartNew в коде, вы можете затем использовать свой TestScheduler в модульном тесте, чтобы протолкнуть все запланированные задачи.

Например, тестируемый код может выглядеть примерно так:

//Task.Factory.StartNew(() => DoSomething())
//    .ContinueWith(t => DoSomethingElse())
Observable.Start(() => DoSomething(), schedulerProvider.ThreadPool)
          .ToTask()
          .ContinueWith(t => DoSomethingElse())

И в вашем модульном тесте:

// ... test code to execute the code under test

// run the tasks on the ThreadPool scheduler
testSchedulers.ThreadPool.Start();

// assertion code can now run