実際に待つことなく、await Task.Delay を持つコンポーネントを単体テストするにはどうすればよいですか。
例えば、
public void Retry()
{
// doSomething();
if(fail)
await Task.Delay(5000);
}
失敗ブランチをテストする必要がありますが、テストを 5 秒間待機させたくありません。
タスク並列ライブラリで利用できるrx 仮想時間ベースのスケジューリングのようなものはありますか?
実際に待つことなく、await Task.Delay を持つコンポーネントを単体テストするにはどうすればよいですか。
例えば、
public void Retry()
{
// doSomething();
if(fail)
await Task.Delay(5000);
}
失敗ブランチをテストする必要がありますが、テストを 5 秒間待機させたくありません。
タスク並列ライブラリで利用できるrx 仮想時間ベースのスケジューリングのようなものはありますか?
タスク並列ライブラリで利用できる rx 仮想時間ベースのスケジューリングのようなものはありますか?
いいえ、ありません。オプションは、テスト スタブで実装できる「タイマー サービス」を定義するか、Microsoft Fakes を使用して への呼び出しをインターセプトすることTask.Delayです。私は後者を好みますが、これは VS Ultimate のみのオプションです。
1) Task.Delay の独自の実装を定義します。
public static class TaskEx
{
private static bool _shouldSkipDelays;
public static Task Delay(TimeSpan delay)
{
return _shouldSkipDelays ? Task.FromResult(0) : Task.Delay(delay);
}
public static IDisposable SkipDelays()
{
return new SkipDelaysHandle();
}
private class SkipDelaysHandle : IDisposable
{
private readonly bool _previousState;
public SkipDelaysHandle()
{
_previousState = _shouldSkipDelays;
_shouldSkipDelays = true;
}
public void Dispose()
{
_shouldSkipDelays = _previousState;
}
}
}
2) コード内のあらゆる場所で、Task.Delay の代わりに TaskEx.Delay を使用します。
3) テストでは、TaskEx.SkipDelays を使用します。
[Test]
public async Task MyTest()
{
using (TaskEx.SkipDelays())
{
// your code that will ignore delays
}
}
Task.Delay のラッパーを作成する alexey anwser に基づいて、Reactive Extensions の IScheduler を使用する Task.Delay を作成する方法を次に示します。これにより、仮想時間を使用して遅延をテストできます。
using System;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
public static class TaskEx
{
public static Task Delay(int millisecondsDelay, CancellationToken cancellationToken = default(CancellationToken))
{
#if TEST
return Observable.Timer(TimeSpan.FromMilliseconds(millisecondsDelay), AppContext.DefaultScheduler).ToTask(cancellationToken);
#else
return Task.Delay(millisecondsDelay, cancellationToken);
#endif
}
}
これは、単体テストを行っていない場合、コンパイル シンボルを使用して Rx を完全に回避します。
AppContext は、スケジューラを参照する単なるコンテキスト オブジェクトです。テストではAppContext.DefaultScheduler = testScheduler、仮想タイム スケジューラによって遅延が発生するように設定できます。
ただし注意点があります。TestScheduler は同期的であるため、タスクを開始して内部で TaskEx.Delay を使用することはできません。これは、タスクがスケジュールされる前にスケジューラが進むためです。
var scheduler = new TestScheduler();
AppContext.DefaultScheduler = scheduler;
Task.Run(async () => {
await TaskEx.Delay(100);
Console.Write("Done");
});
/// this won't work, Task.Delay didn't run yet.
scheduler.AdvanceBy(1);
代わりに、常に を使用してタスクを開始する必要があるObservable.Start(task, scheduler)ため、タスクは次の順序で実行されます。
var scheduler = new TestScheduler();
AppContext.DefaultScheduler = scheduler;
Observable.Start(async () => {
await TaskEx.Delay(100);
Console.Write("Done");
}, scheduler);
/// this runs the code to schedule de delay
scheduler.AdvanceBy(1);
/// this actually runs until the delay is complete
scheduler.AdvanceBy(TimeSpan.FromMilliseconds(100).Ticks);
これは確かにややこしいので、Task.Delay を使用するすべての場所では使用しません。ただし、遅延がアプリの動作を変更する特定のコードがいくつかあり、それをテストする必要があるため、これらの特殊なケースに役立ちます。
John Deters が述べたように、これはコンピューターの時計に対する外部依存関係です (DateTime.UtcNow を呼び出すのは簡単ですが、現在の時刻を取得する必要がある場合と同様に依存関係にあります)。
ただし、常に機能するデフォルト (Task.Delay または DateTime.UtcNow) を指定できるため、これらは特別な依存関係です。したがって、これを行うためのプロパティを持つことができます:
private Func<int, Task> delayer = millisecondsDelay => Task.Delay(millisecondsDelay);
public Func<int, Task> Delayer
{
get { return delayer; }
set { delayer = value ?? (millisecondsDelay => Task.Delay(millisecondsDelay)) }
}
これを使用すると、テストで Task.Delay の呼び出しを置き換えることができます。
sut.Delayer = _ => Task.CompletedTask;
もちろん、これを行うためのクリーンな方法は、インターフェイスを宣言し、コンストラクターを介して取得することです。