Search code examples
c#.netunit-testingxunitfluent-assertions

IEnumerable swallows an exception and a unit test cannot catch it


I'm unit testing the following method and I expect ArgumentOutOfRangeException to be thrown. Instead gridTrading.GridLevels().As<double[]>() returns null for whatever reason. It basically swallows the exception. How do I fix it by keeping the return type as IEnumerable<double> and not converting it to List<double>? Btw, I added .As<double[]>() to prevent multiple enumeration.

Lastly, do you think I'm wrong by trying to keep it IEnumerable<double>?

[Fact]
public void GridLevels_ShouldThrow_WhenGivenInvalidGridType()
{
    // Arrange
    const double lowerLimit = 2000;
    const double upperLimit = 10000;
    const int gridCount = 4;
    const int gridType = 4;

    var gridTrading = new GridTrading(lowerLimit, upperLimit, gridCount, (GridType)gridType);

    // Act
    var action = new Action(() => gridTrading.GridLevels().As<double[]>());

    // Assert
    action.Should().Throw<ArgumentOutOfRangeException>().WithMessage("Grid type must be valid");
}

// The method I test
public IEnumerable<double> GridLevels()
{
    switch (_gridType)
    {
        case GridType.Arithmetic:
            foreach (var level in CalculateArithmetic())
            {
                yield return level;
            }

            break;

        case GridType.Geometric:
            foreach (var level in CalculateGeometric())
            {
                yield return level;
            }

            break;

        default:
            throw new ArgumentOutOfRangeException(nameof(_gridType), "Grid type must be valid");
    }

    IEnumerable<double> CalculateArithmetic()
    {
        var step = (_upperLimit - _lowerLimit) / _gridCount;

        for (var i = 0; i <= _gridCount; i++)
        {
            var price = _lowerLimit + step * i;
            yield return price;
        }
    }

    IEnumerable<double> CalculateGeometric()
    {
        var step = Math.Pow(_upperLimit / _lowerLimit, 1.0 / _gridCount);

        for (var i = 0; i <= _gridCount; i++)
        {
            var price = _lowerLimit * Math.Pow(step, i);
            yield return price;
        }
    }
}

I know I could make GridLevels a List<double> instead IEnumerable<double> which would make it throw for real but I don't wanna do that.

public List<double> GridLevels()
{
    var list = new List<double>();

    switch (_gridType)
    {
        case GridType.Arithmetic:
            list.AddRange(CalculateArithmetic());
            break;

        case GridType.Geometric:
            list.AddRange(CalculateGeometric());
            break;

        default:
            throw new ArgumentOutOfRangeException(nameof(_gridType), "Grid type must be valid");
    }

    return list;

    IEnumerable<double> CalculateArithmetic()
    {
        var step = (_upperLimit - _lowerLimit) / _gridCount;

        for (var i = 0; i <= _gridCount; i++)
        {
            var price = _lowerLimit + step * i;
            yield return price;
        }
    }

    IEnumerable<double> CalculateGeometric()
    {
        var step = Math.Pow(_upperLimit / _lowerLimit, 1.0 / _gridCount);

        for (var i = 0; i <= _gridCount; i++)
        {
            var price = _lowerLimit * Math.Pow(step, i);
            yield return price;
        }
    }
}

Solution

  • You're GridLevels method returns a lazy-evaluated IEnumerable<double> which FA will never explicitly enumerate unless you use the Enumerating extension methods. Instead, you're using the As extension method. That one will try to cast your enumerable to double[], which isn't possible. In that case, As will return default(double[]). In other words, your code isn't being executed.