目标
我想使用Task<T>
使用自定义扩展方法在C#中编写异步工作流。我想对我将使用的每个基本扩展方法进行单元测试,以确保它们正常工作,因此当我开始将它们组合在一起时,行为就不会出现意外。此测试最重要的一个方面是确保这些方法在应用时执行任务,而不是在编写工作流时立即运行,而是仅在等待组合工作流时。
在实践中,我的扩展方法似乎工作正常。但是,我无法创建一个单元测试来测试这些方法的等待方面,而不会阻塞测试线程。
我使用的是Visual Studio 2015,C#5,.NET 4.5.1和NUnit 3。
代码
以下是我想测试的一种扩展方法:
public static async Task<T> Let<T>(this Task<T> source, Action<T> action)
{
var result = await source;
action(result);
return result;
}
以下是我为该方法所做的测试。 (此项目使用Shouldly为NUnit提供了流畅的界面,因此您可以将x.ShouldBe(3)
写为Assert.AreEqual(3, x)
。)
[TestFixture, Category("Task")]
public class WhenLettingTask {
private static bool sourceExecuted;
private static int sideEffectResult;
private static Task<int> Source =>
new Task<int>(() => {
sourceExecuted = true;
return 1;
});
private static readonly Action<int> SideEffect =
n => { sideEffectResult = n; };
[SetUp]
public void TestSetup() {
sourceExecuted = false;
sideEffectResult = 0;
}
[Test]
public void ShouldNotExecuteAnythingImmediately() {
var composed = Source.Let(SideEffect);
sourceExecuted.ShouldBeFalse();
sideEffectResult.ShouldBe(0);
}
[Test]
public async Task ReturnedTaskShouldExecuteInputsOnlyOnce() {
var composed = Source.Let(SideEffect);
var result = await composed; //Blocks forever
sourceExecuted.ShouldBeTrue();
sideEffectResult.ShouldBe(1);
sourceExecuted = false;
sideEffectResult = 0;
for (var i = 0; i < 10; i++) {
result = await composed;
sourceExecuted.ShouldBeFalse();
sideEffectResult.ShouldBe(0);
}
}
}
第一个测试按预期工作,但第二个测试永远阻止第一个await
。
研究
有趣的是,如果我删除了测试方法中的await
,测试将不会阻止。
public static async Task<T> Let<T>(this Task<T> source, Action<T> action)
{
T result = default(T);
action(result);
return result;
}
在寻找有关异步测试的帮助时,我发现有很多帖子建议使用Task.FromResult
来使异步测试工作,但这基本上是等待等待的方面,因为以这种方式创建的Task<T>
以状态RanToCompletion
开头,永远不需要等待。
解决
根据Scott Chamberlain的回答,我将测试夹具中的Source
属性更改为:
private static Task<int> Source =>
Task.Run<int>(() => {
Thread.Sleep(1000);
sourceExecuted = true;
return 1;
});
Task.Run
将立即启动任务,这允许我的第二次测试通过。 Thread.Sleep
是必需的,以便第一次测试仍然通过。
答案 0 :(得分:4)
你永远不会开始任务,你有一个所谓的冷却任务&#34;。 Await不会启动任务,只会等待它们完成。
除非您正在编写任务调度程序,否则永远不需要调用new Task (
,而是在函数返回后使用Task.Run (
创建一个已处于运行状态的热任务。