Как я могу проверить, что оператор LINQ Range отложен?

Я реализовал какой-то оператор диапазона LINQ и хочу провести тест, который проверит, что оператор диапазона действительно отложен.

Методы оператора My Range:

/// <summary>
/// The Range static method, validation part.
/// </summary>
/// <param name = "start">The start.</param>
/// <param name = "count">The count.</param>
/// <returns></returns>
public static IEnumerable<int> Range(int start, int count)
{
    long max = ((long) start) + count - 1;
    if (count < 0 || max > Int32.MaxValue) throw new ArgumentOutOfRangeException(nameof(count));
    return RangeIterator(start, count);
}

/// <summary>
/// The Range operator iterator.
/// </summary>
/// <param name = "start">The start.</param>
/// <param name = "count">The count.</param>
/// <returns></returns>
static IEnumerable<int> RangeIterator(int start, int count)
{
    for (int i = 0; i < count; ++i)
    {
        yield return start + i;
    }
}

Для других отложенных операторов я создал служебный класс ThrowingExceptionEnumerable, который помогает при тестировании:

/// <summary>
/// The class responsible for verifying that linq operator is deferred.
/// </summary>
/// <typeparam name = "T"></typeparam>
public sealed class ThrowingExceptionEnumerable<T> : IEnumerable<T>
{
    /// <summary>
    /// The methods throws <see cref = "InvalidOperationException"/>.
    /// </summary>
    /// <returns></returns>
    public IEnumerator<T> GetEnumerator()
    {
        throw new InvalidOperationException();
    }

    /// <inheritdoc />
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    /// <summary>
    /// The method which checks that the given <see cref = "deferredFunction"/> actually uses deferred execution.
    /// When the function just call itself it should not throw an exception. But, when using the result
    /// by calling <see cref = "GetEnumerator"/> and than GetNext() methods should throws the <see cref = "InvalidOperationException"/>.
    /// </summary>
    /// <typeparam name = "TSource">The deferred function source type.</typeparam>
    /// <typeparam name = "TResult">The deferred function result type.</typeparam>
    /// <param name = "deferredFunction">The deferred function (unit of work under the test).</param>
    public static void AssertDeferred<TSource,TResult>(
        Func<IEnumerable<TSource>, IEnumerable<TResult>> deferredFunction)
    {
        var source = new ThrowingExceptionEnumerable<TSource>();
        
        // Does not throw any exception here, because GetEnumerator() method is not yet used.
        var result = deferredFunction(source);

        // Does not throw InvalidOperationException even here, despite the fact that we retrieve the enumerator.
        using var iterator = result.GetEnumerator();

        Assert.Throws<InvalidOperationException>(() => iterator.MoveNext());
    }

И, например, отложенный оператор Select имеет следующий тест:

/// <summary>
/// Should check that Select operator is deferred.
/// </summary>
[Fact]
public void VerifySelectExecutionIsDeferred()
{
    ThrowingExceptionEnumerable<int>.AssertDeferred<int, int>(source => source.Select(x => x));
}

Первая проблема, с которой я столкнулся при написании такого модульного теста для оператора Range, заключается в том, что Range на самом деле является статическим методом, а не методом расширения. Также дело в том, что у подписи Range нет исходного параметра, поэтому такой подход не может быть использован.

У вас есть умные идеи, как это можно проверить?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
91
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Внешний код не сможет ничего сделать, чтобы убедиться, что значения генерируются на лету. Единственная реальная разница между таким методом и тем, который материализует коллекцию и возвращает ее, заключается в объеме памяти в масштабе, который довольно сложно надежно протестировать в модульном тесте.

Вы можете ясно сказать, что это не так, если посмотреть на код, но вам нужно будет изменить реализацию довольно значительным образом, чтобы получить что-то, что позволит вам проверить это в модульном тесте (например, написание более обобщенного метода «Создать», который использовал делегат для генерации следующего значения).

Если бы у вас было какое-то жесткое требование, чтобы ваша реализация имела модульные тесты для проверки таких вещей, я бы написал такой метод Generate, реализовал бы ваш метод Range, вызвав Generate, написал модульный тест, чтобы убедиться, что Generate не вызывает делегат пока не сгенерирует следующее значение в последовательности, а затем утверждает, что метод Range откладывает выполнение, потому что он использует Generate для создания своей последовательности. Я бы не хотел делать это в производственном коде, это действительно был бы просто способ выполнить требование и принести некоторые жертвы в удобочитаемости и (мягкой) производительности ради этого.

Отлично, твой ответ многое для меня проясняет. Нет, у меня таких требований нет, я просто играюсь с TDD и переписываю LINQ, и мне просто было очень любопытно, можно ли провести такой тест (теперь я вижу, что это не имеет особого смысла).

Omelian Levkovych 14.12.2020 20:29

Другие вопросы по теме