Как выполнить модульное тестирование, чтобы задачи выполнялись синхронно - PullRequest
0 голосов
/ 05 февраля 2020

В моем коде у меня есть метод, такой как:

void PerformWork(List<Item> items)
{
    HostingEnvironment.QueueBackgroundWorkItem(async cancellationToken =>
    {
        foreach (var item in items)
        {
            await itemHandler.PerformIndividualWork(item);
        }
    });
}

Где Item - это просто известная модель, а itemHandler просто выполняет некоторую работу на основе модели (класс ItemHandler). определяется в отдельной поддерживаемой кодовой базе как nuget pkg, я бы не хотел его изменять). Цель этого кода - выполнить работу для списка элементов в фоновом режиме, но синхронно.

В рамках этой работы я хотел бы создать модульный тест для проверки того, что при вызове этого метода элементы обрабатываются синхронно. Я почти уверен, что проблему можно упростить до следующего:

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, а затем либо проверка времени, прошедшего для всей фоновой задачи, находящейся в очереди, либо проверка временных отметок itemHandler полученных вызовов для минимальное время между звонками. Например, если у меня PerformIndividualWork смоделировано, чтобы задержать 500 миллисекунд, и я ожидаю три элемента, то я могу проверить истекшее время:

stopwatch.Start();
// I have an interface instead of directly calling HostingEnvironment, so I can access the task being queued here
backgroundTask.Invoke(...);
stopwatch.Stop();

Assert.IsTrue(stopwatch.ElapsedMilliseconds > 1500);

Но это не так и может привести к ложному позитивы. Возможно, решение заключается в модификации самого кода; тем не менее, я не могу придумать, как осмысленно изменить его, чтобы сделать возможным такого рода модульное тестирование (задачи тестирования выполняются по порядку). Мы обязательно проведем системное / интеграционное тестирование, чтобы гарантировать, что проблема, вызванная асинхронной производительностью отдельных элементов, не возникнет, но я бы хотел провести тестирование здесь и на этом уровне.

1 Ответ

0 голосов
/ 06 февраля 2020

Не уверен, что это хорошая идея, но одним из подходов может быть использование 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}");
            }
        });
}

Возможно, с этим большие проблемы, но основная идея c состоит в том, чтобы проверить, сколько элементов уже обрабатывается при вводе метода, затем уменьшить счетчик и проверка, что все еще нет других предметов, которые обрабатываются. С многопоточностью я думаю, что очень сложно дать гарантии о вещах только на основе тестов, но с достаточным количеством обработанных элементов это может дать нам небольшую уверенность в том, что все работает как положено:

[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, тогда возможно это стоит использовать более явно синхронный дизайн, например, очередь, обслуживаемую только одним потоком, так что вы гарантированно гарантируете синхронное выполнение по проекту, и люди не будут непреднамеренно менять его на asyn c во время рефакторинга (т.е. он намеренно синхронен и задокументировано таким образом).

...