【问题标题】:Property Based Testing in F# using conditional parameters在 F# 中使用条件参数进行基于属性的测试
【发布时间】:2019-07-15 21:13:02
【问题描述】:

我目前正在编写一个基于属性的测试来测试 f# 中具有 4 个浮点参数的速率计算函数,并且所有参数都有特定的条件才能使它们有效(例如,a > 0.0 && a 一)。我确实有一个函数检查是否满足这些条件并返回一个布尔值。我的问题是,在我在 FsCheck.Xunit 中使用 [Property>] 的测试代码中,如何限制生成器仅使用满足我的参数特定条件的值来测试代码?

【问题讨论】:

    标签: unit-testing f# xunit fscheck property-based-testing


    【解决方案1】:

    如果您使用 FsCheck,则可以使用 Gen.filter 函数和 Gen.map 函数。

    假设你有这个函数 funToBeTested 你正在测试,它需要一个

    let funToBeTested a b = if a < b then a + b else failwith "a should be less than b" 
    

    您正在测试funToBeTested 与输入成正比的属性:

    let propertyTested a b = funToBeTested a b / 2. = funToBeTested (a / 2.) (b / 2.)
    

    您还有一个谓词来检查 a & b 的条件要求:

    let predicate a b = a > 0.0 && a < 1.0 && b > a
    

    我们首先使用Gen.chooseGen.map 生成float 数字,这种方式已经生成了仅从0.0 到1.0 的值:

    let genFloatFrom0To1 = Gen.choose (0, 10000) |> Gen.map (fun i -> float i / 10000.0 )
    

    然后我们使用上面的predicate函数生成two从0到1的浮点数和filter它们

    let genAB            = Gen.two genFloatFrom0To1 |> Gen.filter (fun (a,b) -> predicate a b )
    

    现在我们需要创建一个新类型 TestData 来使用这些值:

    type TestData = TestData of float * float
    

    我们将结果值映射到TestData

    let genTest = genAB |> Gen.map TestData
    

    接下来,我们需要将genTest 注册为TestData 的生成器,为此我们创建一个具有Arbitrary&lt;TestData&gt; 类型的静态成员的新类:

    type MyGenerators =
        static member TestData : Arbitrary<TestData> = genTest |> Arb.fromGen
    
    Arb.register<MyGenerators>() |> ignore
    

    最后我们使用TestData作为输入来测试属性:

    Check.Quick (fun (TestData(a, b)) -> propertyTested a b )
    

    更新:

    组合不同生成器的一种简单方法是使用gen 计算表达式:

    type TestData = {
        a : float
        b : float 
        c : float 
        n : int
    }
    
    let genTest = gen {
        let! a = genFloatFrom0To1
        let! b = genFloatFrom0To1
        let! c = genFloatFrom0To1
        let! n = Gen.choose(0, 30)
        return {
            a = a
            b = b
            c = c
            n = n
        }
    }
    
    type MyGenerator =
        static member TestData : Arbitrary<TestData> = genTest |> Arb.fromGen
    
    Arb.register<MyGenerator>() |> ignore
    
    
    let ``Test rate Calc`` a b c n =                               
        let r = rCalc a b c
        (float) r >= 0.0 && (float) r <= 1.0    
    
    
    Check.Quick (fun (testData:TestData) -> 
        ``Test rate Calc`` 
            testData.a 
            testData.b 
            testData.c 
            testData.n)         
    

    【讨论】:

    • 嗨,我在我的代码中遵循了这个逻辑,但我不得不使用 [|])>] 而不是 Check.Quick,因为我一直收到“不”当我使用 Check.Quick 运行时,可以找到测试。但是,当我运行测试时,它一直失败,因为测试中生成的数字仍然超出范围(我经常在 a,b 之一中得到 -infinity)。我怀疑这是因为 [] 子句以某种方式无法正常运行。你知道可能会发生什么吗?再次感谢
    • 你的属性函数Test rate Calc需要接收TestData(a, b, c)作为参数,目前它接收a b c
    • 有效!谢谢!如果您不介意我问另一个问题,如果我想在 TestData 类型中附加 Gen.choose(1,30) 中的另一个 int 变量,那么 testdata 将是 float* float * float*int,我该如何修改 genTest或 genAB 映射新的 TestData 类型?
    • 我在答案末尾添加了一种方法。我还将TestData从DU更改为记录,因为当DU中的字段太多时它会变得混乱。
    【解决方案2】:

    @AMieres 的回答很好地解释了解决此问题所需的一切!

    一个小的补充是,如果谓词不适用于生成器生成的大量元素,则使用Gen.filter 可能会很棘手,因为生成器需要运行很长时间,直到找到足够数量的有效元素元素。

    在@AMieres 的示例中,这很好,因为生成器已经生成了正确范围内的数字,因此它只检查第二个是否更大,大约一半的随机生成对都是这种情况。

    如果您可以这样编写,以便始终生成有效值,那就更好了。对于这种特殊情况,我的版本是使用map 交换数字,以便始终将较小的数字放在第一位:

    let genFloatFrom0To1 = Gen.choose (0, 10000) |> Gen.map (fun i -> float i / 10000.0 )
    let genAB = Gen.two genFloatFrom0To1 |> Gen.map (fun (a, b) -> min a b, max a b)
    

    【讨论】:

    • 这是一个优雅的解决方案!
    猜你喜欢
    • 2010-11-02
    • 2014-09-13
    • 2022-01-16
    • 2019-11-02
    • 2011-08-27
    • 2016-03-01
    • 2019-02-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多