单元测试循环复杂但其他微不足道的计算

ced*_*edd 5 unit-testing test-double

假设我有一个计算器类主要功能是执行以下操作(此代码已简化以使讨论更容易,请不要评论它的样式)

double pilingCarpetArea = (hardstandingsRequireRemediation = true) ? hardStandingPerTurbineDimensionA * hardStandingPerTurbineDimensionB * numberOfHardstandings * proportionOfHardstandingsRequiringGroundRemediationWorks : 0;

double trackCostMultipler;
if (trackConstructionType = TrackConstructionType.Easy) trackCostMultipler = 0.8
else if (trackConstructionType = TrackConstructionType.Normal) trackCostMultipler = 1
else if (trackConstructionType = TrackConstructionType.Hard) trackCostMultipler = 1.3
else throw new OutOfRangeException("Unknown TrackConstructionType: " + trackConstructionType.ToString());

double PilingCostPerArea = TrackCostPerMeter / referenceTrackWidth * trackCostMultipler;
Run Code Online (Sandbox Code Playgroud)

我应该测试至少7条通过这个类的路由,trackCostMultiplier和hardstandingsRequireRemediation(6种组合)和异常条件的组合.我可能还想添加一些除以零和溢出等等,如果我感觉很敏锐.

到目前为止,我可以轻松,时尚地测试这些组合.实际上,我可能相信乘法和加法不太可能出错,因此只需要对trackCostMultipler进行3次测试,为hardstandingsRequireRemediation进行2次测试,而不是测试所有可能的组合.

然而,这是一个简单的案例,我们的应用程序中的逻辑很可能在环境上比这复杂得多,因此测试的数量可能会增长很多.

有一些方法可以解决这种复杂性

  1. 将trackCostMultipler计算提取到同一类中的方法

这是一件好事,但它不能帮助我测试它,除非我公开这个方法,这是"生产中的测试逻辑"的一种形式.我经常以实用主义的名义这样做,但如果可以,我想避免.

  1. 将trackCostMultipler计算推迟到另一个类

如果计算足够复杂,这似乎是一件好事,我可以轻松地测试这个新类.但是我刚刚对原始类的测试更加复杂,因为我现在想要传递某种类型的ITrackCostMultipler"Test Double",检查它是否使用正确的参数调用,并检查它的返回值是否被使用正确.当一个类有十个子计算器时,它的单元/积分测试变得非常大并且难以理解.

我同时使用(1)和(2),他们给了我信心,他们使调试更快.然而,肯定存在缺点,例如生产中的测试逻辑和模糊测试.

我想知道其他人测试循环复杂代码的经验是什么?有没有办法做到这一点没有缺点?我意识到测试特定子类可以解决(1),但这对我来说似乎是一种遗留技术.也可以操作输入,以便计算的各个部分返回0(用于加法或减法)或1(用于乘法或除法)以使测试更容易,但这只能让我到目前为止.

谢谢

土木工程拓展署

Mar*_*ann 2

继续从注释到OP的讨论,如果你有引用透明的函数,你可以先单独测试每个小部分,然后将它们组合起来并测试组合是否正确。

由于组成函数是引用透明的,因此它们在逻辑上可以与其返回值互换。现在唯一剩下的步骤是证明整体函数正确地组成了各个函数。

它非常适合基于属性的测试

例如,假设您有一个复杂计算的两个部分:

module MyCalculations =
    let complexPart1 x y = x + y // Imagine it's more complex

    let complexPart2 x y = x - y // Imagine it's more complex
Run Code Online (Sandbox Code Playgroud)

这两个函数都是确定性的,因此假设您确实想测试由facade这两个函数组成的函数,则可以定义此属性:

open FsCheck.Xunit
open Swensen.Unquote
open MyCalculations

[<Property>]
let facadeReturnsCorrectResult (x : int) (y : int) =
    let actual = facade x y

    let expected = (x, y) ||> complexPart1 |> complexPart2 x
    expected =! actual
Run Code Online (Sandbox Code Playgroud)

与其他基于属性的测试框架一样,FsCheck将抛出大量随机生成的值facadeReturnsCorrectResult(默认情况下为 100 次)。

鉴于 和complexPart1都是complexPart2确定性的,但您不知道xy是什么,通过测试的唯一方法是正确实现该函数:

let facade x y = 
    let intermediateResult = complexPart1 x y
    complexPart2 x intermediateResult
Run Code Online (Sandbox Code Playgroud)