在我的代码中,我有一个方法,例如:
void PerformWork(List<Item> items)
{
HostingEnvironment.QueueBackgroundWorkItem(async cancellationToken =>
{
foreach (var item in items)
{
await itemHandler.PerformIndividualWork(item);
}
});
}
在Item
只是一个已知模型的情况下,itemHandler
只是基于该模型进行了一些工作(ItemHandler
类在单独维护的代码库中定义为nuget pkg I' d而不是不修改)。
这段代码的目的是在后台同步处理列表中的项目。
作为工作的一部分,我想创建一个单元测试以验证在调用此方法时,项目是同步处理的。我敢肯定,这个问题可以简化为:
await MyTask(1);
await MyTask(2);
Assert.IsTrue(/* MyTask with arg 1 was completed before MyTask with arg 2 */);
我可以轻松进行单元测试的这段代码的第一部分是保持序列。例如,使用NSubstitute,我可以检查库代码上的方法调用顺序:
Received.InOrder(() =>
{
itemHandler.PerformIndividualWork(Arg.Is<Item>(arg => arg.Name == "First item"));
itemHandler.PerformIndividualWork(Arg.Is<Item>(arg => arg.Name == "Second item"));
itemHandler.PerformIndividualWork(Arg.Is<Item>(arg => arg.Name == "Third item"));
});
但是我不太确定如何确保它们不会并行运行。我有几个想法似乎很糟糕,例如嘲笑该库以使在调用PerformIndividualWork
时出现人为延迟,然后检查正在排队的整个后台任务的时间或检查{{1}的时间戳}在两次通话之间的最短时间内接听电话。例如,如果我模拟了itemHandler
来延迟500毫秒,而我期望有3个项目,那么我可以检查经过的时间:
PerformIndividualWork
但这感觉不对,并可能导致误报。解决的办法可能在于修改代码本身。但是,我想不出一种有意义地更改它的方法,以使这种单元测试(测试任务按顺序运行)成为可能。我们肯定会进行系统/集成测试,以确保不会发生由单个项目的异步性能引起的问题,但是我也想在此级别进行测试。
答案 0 :(得分:0)
不确定这是否是一个好主意,但是一种方法可能是使用itemHandler
来检测何时并行处理项目。这是一个简单又肮脏的例子:
public class AssertSynchronousItemHandler : IItemHandler
{
private volatile int concurrentWork = 0;
public List<Item> Items = new List<Item>();
public Task PerformIndividualWork(Item item) =>
Task.Run(() => {
var result = Interlocked.Increment(ref concurrentWork);
if (result != 1) {
throw new Exception($"Expected 1 work item running at a time, but got {result}");
}
Items.Add(item);
var after = Interlocked.Decrement(ref concurrentWork);
if (after != 0) {
throw new Exception($"Expected 0 work items running once this item finished, but got {after}");
}
});
}
这可能有很大的问题,但是基本思想是,当我们进入该方法时,要检查已经处理了多少个项目,然后递减计数器并检查仍然没有其他项目要处理。我认为使用线程化的东西很难保证仅通过测试就可以保证,但是如果处理了足够多的项,这可以使我们稍微放心,它可以按预期工作:
[Fact]
public void Sample() {
var handler = new AssertSynchronousItemHandler();
var subject = new Subject(handler);
var input = Enumerable.Range(0, 100).Select(x => new Item(x.ToString())).ToList();
subject.PerformWork(input);
// With the code from the question we don't have a way of detecting
// when `PerformWork` finishes. If we can't change this we need to make
// sure we wait "long enough". Yes this is yuck. :)
Thread.Sleep(1000);
Assert.Equal(input, handler.Items);
}
如果我修改PerformWork
以并行执行操作,则会导致测试失败:
public void PerformWork2(List<Item> items) {
Task.WhenAll(
items.Select(item => itemHandler.PerformIndividualWork(item))
).Wait(2000);
}
// ---- System.Exception : Expected 1 work item running at a time, but got 4
也就是说,如果同步运行非常重要,并且通过使用async/await
看一眼实现并不明显,那么也许值得使用更明显的同步设计,例如仅由一个线程提供服务的队列,这样就可以确保通过设计确保同步执行,并且人们在重构期间不会无意间将其更改为异步(即,故意进行同步并以此方式进行记录)。