保留IEnumerable<double>时单元测试无法捕获异常的解决及疑问
我在对GridLevels方法做单元测试时,预期它会抛出ArgumentOutOfRangeException,但调用gridTrading.GridLevels().As<double[]>()时却返回null,异常被吞掉了。我想在**保持返回类型为IEnumerable<double>(不转成List<double>)**的前提下解决这个问题,另外加.As<double[]>()是为了避免多次枚举。最后想问问,坚持用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"); }
待测试的原方法
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; } } }
可正常抛出异常但不想用的版本
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; } } }
这是因为使用yield return的迭代器方法是延迟执行的:GridLevels里的switch逻辑(包括default分支的异常抛出)要等到迭代器被枚举时才会执行。但As<double[]>()在处理未被枚举的空迭代器时,直接返回了null,根本没触发枚举流程,导致异常完全没机会抛出。
IEnumerable<double>) 要让参数校验的异常在调用方法时立刻抛出(而非延迟到枚举阶段),需要把校验逻辑从迭代器执行流程中抽出来,提前到方法开头执行。下面提供两种可行的修改方式:
方式一:提前添加显式校验
public IEnumerable<double> GridLevels() { // 提前校验_gridType合法性,调用方法时立刻抛出异常 if (!Enum.IsDefined(typeof(GridType), _gridType)) { throw new ArgumentOutOfRangeException(nameof(_gridType), "Grid type must be valid"); } 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; } 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; } } }
方式二:拆分校验与迭代器逻辑
如果想保留switch的default分支来抛出异常,可以把校验和迭代器返回逻辑拆分开:
public IEnumerable<double> GridLevels() { // 先执行switch做校验,触发异常 switch (_gridType) { case GridType.Arithmetic: case GridType.Geometric: break; default: throw new ArgumentOutOfRangeException(nameof(_gridType), "Grid type must be valid"); } // 再返回对应的迭代器 return _gridType switch { GridType.Arithmetic => CalculateArithmetic(), GridType.Geometric => CalculateGeometric(), _ => Enumerable.Empty<double>() // 理论上不会走到这里,因为前面已校验 }; 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; } } }
两种方式都能让异常在调用GridLevels()时立刻抛出,同时保持返回类型为IEnumerable<double>,后续调用.As<double[]>()也能正常触发枚举,不会吞掉异常。
IEnumerable<double>的合理性 是否合理取决于你的具体使用场景:
- 适合使用的场景:
- 数据量较大,延迟执行可以节省内存(无需一次性加载所有元素)
- 仅需枚举一次,或者希望将计算延迟到真正需要数据的时候
- 希望保持接口抽象性,不暴露具体集合类型(如List),方便后续修改内部实现
- 不适合的场景:
- 需要多次枚举数据:每次枚举都会重新执行迭代器逻辑(比如
CalculateArithmetic会重新计算一遍),若不想重复计算,要么在方法内部缓存结果为数组/List再返回,要么在调用方提前转换 - 需要立刻完成参数校验并抛出异常:原生迭代器方法会延迟执行校验,需额外处理才能提前抛出
- 需要集合的额外功能(如索引访问、添加/删除元素):
IEnumerable仅支持枚举,没有这些操作
- 需要多次枚举数据:每次枚举都会重新执行迭代器逻辑(比如
如果你的场景是数据量不大,且希望保持接口抽象性,或仅需枚举一次,那么坚持使用IEnumerable<double>是合理的。若存在多次枚举的情况,建议要么在方法内部缓存结果(仍返回IEnumerable<double>),要么在调用方提前转成数组/List避免重复计算。
内容的提问来源于stack exchange,提问作者nop

