Модульный тест для асинхронного ограничителя скорости периодически не проходит - PullRequest
0 голосов
/ 25 января 2019

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

public class TaskRateLimiter : ITaskRateLimiter
{
    private SlidingWindow _window;

    public TaskRateLimiter(int maxCount, TimeSpan time)
    {
        _window = new SlidingWindow(maxCount, time);
    }

    public async Task RateLimit(Func<Task> function)
    {
        await _window.WaitExecute();
        await function();
    }

    public async Task<T> RateLimit<T>(Func<Task<T>> function)
    {
        await _window.WaitExecute();
        return await function();
    }

    private class SlidingWindow
    {
        private int _count;
        private TimeSpan _time;
        private Queue<DateTime> _window = new Queue<DateTime>();
        private SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

        public SlidingWindow(int count, TimeSpan time)
        {
            _count = count;
            _time = time;
        }

        private void RefreshWindow()
        {
            var now = DateTime.Now;

            while (_window.Count > 0 && _window.Peek().Add(_time) < now)
            {
                _window.Dequeue();
            }
        }

        private bool CanExecute()
        {
            RefreshWindow();
            return _window.Count < _count;
        }

        public async Task WaitExecute()
        {
            await _semaphore.WaitAsync();

            try
            {
                if (!CanExecute())
                {
                    await Task.Delay(_window.Peek().Add(_time).Subtract(DateTime.Now));
                }

                _window.Enqueue(DateTime.Now);
            }

            finally
            {
                _semaphore.Release();
            }
        }
    }
}

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

[TestClass]
public class TaskRateLimiterTests
{
    private int _numEvents;
    private TimeSpan _timeSpan;
    private TaskRateLimiter _rateLimiter;
    private Stopwatch _stopWatch;
    private List<int> _testList;
    private Func<Task> _funcToLimit;
    private Func<Task<int>> _funcWithReturnToLimit;

    [TestInitialize]
    public void Setup()
    {
        _numEvents = 10;
        _timeSpan = TimeSpan.FromMilliseconds(50);
        _rateLimiter = new TaskRateLimiter(_numEvents, _timeSpan);
        _stopWatch = new Stopwatch();
        _testList = new List<int>();
        _funcWithReturnToLimit = new Func<Task<int>>(() => { _testList.Add(5); return Task.FromResult(5); });
    }

    [TestMethod]
    public async Task RateLimit_NotExceeded()
    {
        var tempList = new List<int>();

        _stopWatch.Start();

        for (int i = 0; i < _numEvents + 1; i++)
        {
            tempList.Add(await _rateLimiter.RateLimit(_funcWithReturnToLimit));
        }

        _stopWatch.Stop();

        Assert.IsTrue(_stopWatch.Elapsed > _timeSpan)
        Assert.IsTrue(tempList.All(x => x.Equals(5)));
    }
}

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

фактический: 492630, нижний предел: 500000

, где прошедшие отметки секундомера находятся чуть ниже нижнего уровня.Я подозреваю, что функция с нетривиальным временем выполнения исправит тест.Однако, это не говорит мне, почему это периодически терпит неудачу, это из-за неточности в секундомере?Есть ли лучший способ для меня, чтобы измерить эти временные интервалы?Есть ли ошибка в тестируемом коде, которую я пропускаю?Я обычно вижу, что это происходит ~ 5 раз за 10000.

...