Модульное тестирование асинхронной операции
Я хочу выполнить модульное тестирование метода, который у меня есть, который выполняет и асинхронную операцию:
Task.Factory.StartNew(() =>
{
// method to test and return value
var result = LongRunningOperation();
});
В моем модульном тесте я пишу необходимые методы и т. Д. (Написанные на C#), но проблема в том, что асинхронная операция не завершена, прежде чем я выполню тест.
Как я могу обойти это? Должен ли я создать макет TaskFactory или любые другие советы для модульного тестирования асинхронной операции?
4 ответа
Вы должны иметь какой-то способ подделать создание задачи.
Если вы переместили Task.Factory.StartNew
вызов к некоторой зависимости (ILongRunningOperationStarter
) тогда вы могли бы создать альтернативную реализацию, которая использовала TaskCompletionSource
создавать задачи, которые выполняются именно там, где вы хотите их.
Это может стать немного волосатым, но это может быть сделано. Некоторое время назад я писал об этом - модульное тестирование метода, с которого начинались задачи, что, конечно, облегчало задачу. Это в контексте async/await в C# 5, но применяются те же принципы.
Если вы не хотите подделывать всю задачу создания задачи, вы можете заменить фабрику задач и таким образом контролировать время - но я подозреваю, что это будет даже более опасно, если честно.
Я бы предложил заглушить TaskScheduler в вашем методе со специальной реализацией для модульных тестов. Вам необходимо подготовить свой код для использования внедренного TaskScheduler:
private TaskScheduler taskScheduler;
public void OperationAsync()
{
Task.Factory.StartNew(
LongRunningOperation,
new CancellationToken(),
TaskCreationOptions.None,
taskScheduler);
}
В своем модульном тесте вы можете использовать DeterministicTaskScheduler, описанный в этом сообщении в блоге, чтобы запустить новую задачу в текущем потоке. Ваша асинхронная операция будет завершена до того, как вы нажмете свой первый оператор assert:
[Test]
public void ShouldExecuteLongRunningOperation()
{
// Arrange: Inject task scheduler into class under test.
DeterministicTaskScheduler taskScheduler = new DeterministicTaskScheduler();
MyClass mc = new MyClass(taskScheduler);
// Act: Let async operation create new task
mc.OperationAsync();
// Act: Execute task on the current thread.
taskScheduler.RunTasksUntilIdle();
// Assert
...
}
Попробуйте что-то вроде этого...
object result = null;
Task t = Task.Factory.StartNew(() => result = LongRunningThing());
Task.Factory.ContinueWhenAll(new Task[] { t }, () =>
{
Debug.Assert(result != null);
});
Установите пользовательский интерфейс и графики фоновых задач и замените их в модульном тесте на этот.
Ниже код был скопирован из интернета, извините за отсутствующую ссылку на автора:
public class CurrentThreadTaskScheduler : TaskScheduler
{
protected override void QueueTask(Task task)
{
TryExecuteTask(task);
}
protected override bool TryExecuteTaskInline(
Task task,
bool taskWasPreviouslyQueued)
{
return TryExecuteTask(task);
}
protected override IEnumerable<Task> GetScheduledTasks()
{
return Enumerable.Empty<Task>();
}
public override int MaximumConcurrencyLevel => 1;
}
Итак, чтобы проверить код:
public TaskScheduler TaskScheduler
{
get { return taskScheduler ?? (taskScheduler = TaskScheduler.Current); }
set { taskScheduler = value; }
}
public TaskScheduler TaskSchedulerUI
{
get { return taskSchedulerUI ?? (taskSchedulerUI = TaskScheduler.FromCurrentSynchronizationContext()); }
set { taskSchedulerUI = value; }
}
public Task Update()
{
IsBusy = true;
return Task.Factory.StartNew( () =>
{
LongRunningTask( );
}, CancellationToken.None, TaskCreationOptions.None, TaskScheduler )
.ContinueWith( t => IsBusy = false, TaskSchedulerUI );
}
Вы напишете следующий юнит-тест:
[Test]
public void WhenUpdateThenAttributeManagerUpdateShouldBeCalled()
{
taskScheduler = new CurrentThreadTaskScheduler();
viewModel.TaskScheduler = taskScheduler;
viewModel.TaskSchedulerUI = taskScheduler;
viewModel.Update();
dataManagerMock.Verify( s => s.UpdateData( It.IsAny<DataItem>>() ) );
}