如何对任务同步运行进行单元测试

时间:2020-02-05 18:37:30

标签: c# unit-testing task nunit nsubstitute

在我的代码中,我有一个方法,例如:

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

但这感觉不对,并可能导致误报。解决的办法可能在于修改代码本身。但是,我想不出一种有意义地更改它的方法,以使这种单元测试(测试任务按顺序运行)成为可能。我们肯定会进行系统/集成测试,以确保不会发生由单个项目的异步性能引起的问题,但是我也想在此级别进行测试。

1 个答案:

答案 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看一眼实现并不明显,那么也许值得使用更明显的同步设计,例如仅由一个线程提供服务的队列,这样就可以确保通过设计确保同步执行,并且人们在重构期间不会无意间将其更改为异步(即,故意进行同步并以此方式进行记录)。