Как выполнить модульное тестирование отчета о ходе выполнения асинхронного вызова?

Цель

Я хочу проверить отчет о ходе выполнения некоторого асинхронного вызова, который принимает IProgress. Я хочу проверить:

  • если этот сообщаемый прогресс всегда находится в диапазоне [0,0; 1.0]
  • имеет возрастающие значения (последнее значение ‹= следующее значение)
  • сообщает 1.0 в какой-то момент, например, как последний вызов

Я хочу использовать как можно меньше фреймворков — было бы неплохо решить проблему без использования другой сторонней библиотеки.

Проблема/Мои попытки

Я попытался написать простой метод тестирования асинхронной задачи для достижения этой цели. Я вызываю метод для тестирования с ожиданием, например. await asyncCallToTest(new Progress((p) => { Assert.someting(p); })). И, возможно, использовать переменные стека в лямбде. Полные примеры позже.

Однако это привело к тому, что тесты прошли успешно, даже если это было невозможно, или иногда тест прошел, а затем не прошел без изменений....

Код

Я воссоздал проблемы с помощью простого автономного тестового класса:

using System;
using System.Threading.Tasks;

using NUnit.Framework;

namespace FastProgressReporter
{
    public class Tests
    {
        [Test]
        public async Task SampleOkTest()
        {
            await TestReporter(new Progress<double>((p) => {
                Assert.GreaterOrEqual(p, 0);
                Assert.LessOrEqual(p, 1);
            }));
            //Sometimes fails, sometims passes. Why?
        }

        [Test]
        public async Task SampleFailTest()
        {
            await TestReporter(new Progress<double>((p) => {
                Assert.GreaterOrEqual(p, 2); // Has to Fail, however passes.
                Assert.LessOrEqual(p, 3);
            }));
        }

        [Test]
        public async Task ReportsIncremental()
        {
            double latestProgress = 0;
            await TestReporter(new Progress<double>((p) => {
                p = 1 - p; //Force Decrement...
                Assert.GreaterOrEqual(latestProgress, p); //Should fail on second call
                latestProgress = p;
            }));
        }

        [Test]
        public async Task ReportsOneOneOrMoreTimes()
        {
            double maxProgress = 0;
            await TestReporter(new Progress<double>((p) => {
                maxProgress = p > maxProgress ? p : maxProgress;
            }));
            Assert.AreEqual(0, maxProgress); //Should always fail
            //Sometimes fails, sometims passes. Why?
        }

        // Methode under test
        public static async Task TestReporter(IProgress<double> progress)
        {
            progress.Report(0);
            await Task.Run(() => { /*Do nothing, however await*/ });
            progress.Report(0.5);
            await Task.Run(() => { /*Do nothing, however await*/ });
            progress.Report(1);
        }
    }
}

Окружающая среда

VisualStudio 2019, .net-framework 4.7.2, тестовый проект NUnit3

Квеситон

Как мне надежно и правильно достигать своих целей?

Связанные вопросы

Проверка хода выполнения, о котором сообщает асинхронный вызов правильно сообщается в модульных тестах. Здесь используется сторонняя библиотека, которую я пытаюсь избежать, и вызов .Report() непосредственно внутри тестового метода, а не в асинхронном механизме, вызываемом тестом.

Спасибо за помощь.


person ProfBits    schedule 27.05.2021    source источник
comment
ты, исправлено @Fildor   -  person ProfBits    schedule 27.05.2021
comment
Я бы попробовал следующее: пусть Progress добавит все значения в список. Затем сделайте свои утверждения по списку. (Должен быть отсортирован, не должен быть пустым, должен иметь X элементов, первый элемент должен быть A, последний элемент должен быть B...)   -  person Fildor    schedule 27.05.2021
comment
И я бы использовал await Task.Delay(someMillis); вместо await Task.Run( () => { /* Nothing really */ });   -  person Fildor    schedule 27.05.2021
comment
Либо используйте Task.Delay или Task.Yield вместо Task.Run. Это небольшое изменение, но эти методы лучше подходят для того, чего вы хотите достичь с помощью Task.Run.   -  person Andrei15193    schedule 27.05.2021


Ответы (2)


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

Что касается ваших тестов, это странно, потому что, если они выполняются правильно, они должны иметь один и тот же результат каждый раз, когда вы их запускаете, потому что нет зависимости от состояния. Единственная зависимость заключается в том, что средство запуска тестов фактически запускает асинхронные тесты. NUnit должен уметь это делать, насколько я знаю, он поддерживает асинхронные тесты.

Экземпляр Progress фиксирует текущий SynchronizationContext и выполняет обратный вызов, используя его. Это особенно полезно для пользовательского интерфейса, поскольку обратный вызов прогресса будет выполняться в потоке пользовательского интерфейса, поэтому вам не нужно об этом беспокоиться. Вполне вероятно, что NUnit имеет свой собственный SynchronizationContext для выполнения асинхронных тестов, и ваши тесты иногда терпят неудачу, а иногда могут быть связаны с отчетами о ходе выполнения, выполняемыми после фактического завершения соответствующего теста. В этом случае вы можете фактически получить исключение утверждения одного теста в другом тесте.

Если это так, то вам может понадобиться собственная реализация IProgress, которая не использует SynchronizationContext, чтобы должным образом выполнять отчеты о ходе выполнения. По сути, вы хотите, чтобы ваш обратный вызов выполнялся при вызове метода отчета. Ваша реализация может сделать именно это: всякий раз, когда вы вызываете метод progress, он будет добавлять сообщенное значение в список, который отображается через свойство, а затем выполнять утверждения с использованием этого свойства.

Что дает вам странные результаты, так это, скорее всего, использование класса Progress, поскольку он внутренне зависит от SynchronizationContext для выполнения обратного вызова.

person Andrei15193    schedule 27.05.2021
comment
Хорошо, я тестирую это, добавляя сообщаемый прогресс в список, а затем утверждать -> все еще случайные и неправильные результаты ``` [Test] public async Task SampleFailTest() { List‹double› reportData = new List‹double›() ; await TestReporter(new Progress‹double›((p) =› { reportData.Add(p); })); foreach (var e в ReportData) { Assert.GreaterOrEqual(2, e); // Должен завершиться ошибкой, но все же может пройти. Assert.LessOrEqual(-1, e); } } ``` и yes может быть самим классом Progress - person ProfBits; 28.05.2021
comment
@ProfBits, скорее всего, это экземпляр Progress. Попробуйте с пользовательской реализацией IProgress, это только один метод. - person Andrei15193; 28.05.2021

Решение

Как предложил @Andrei15193, я попытался реализовать IProgress самостоятельно, чтобы избавиться от класса Progress.

private class UnitTestProgress<T> : IProgress<T>
{
    private Action<T> handler;

    public UnitTestProgress(Action<T> handler)
    {
        this.hander = handler?? throw new ArgumentNullException(nameof(hander));
    }

    public void Report(T value)
    {
        handler(value);
    }
}

Я выбрал здесь очень простой класс в качестве реализации.

Насколько я проверял, если Progress заменить моим новым классом, проблемы будут решены, а юнит-тесты надежны.

person ProfBits    schedule 28.05.2021