Я реализовал какой-то оператор диапазона 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 нет исходного параметра, поэтому такой подход не может быть использован.
У вас есть умные идеи, как это можно проверить?





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