【问题标题】:How to pass FsCheck Test Correctly如何正确通过 FsCheck 测试
【发布时间】:2018-11-22 13:15:37
【问题描述】:
let list p = if List.contains " " p || List.contains null p then false else true

我有这样一个功能来检查列表是否格式正确。该列表不应包含空字符串和空值。由于Check.Verbose list 返回可伪造的输出,我没有得到我所缺少的东西。

我应该如何解决这个问题?

【问题讨论】:

  • 请发布完整的MCVE。我们可以看到该属性,显然可以通过[null] 之类的输入来证伪。 string list 的默认生成器将产生这样的输入。问题是您如何生成输入以及如何使用它们。
  • 你有生成列表的功能吗?如果你这样做了,那么这个函数加上这个函数就是你应该传递给Check.Verbose的函数。函数list 是一个验证函数,所以它当然必须是可以通过任何使其返回false 的输入来伪造的。你使用这个函数来测试一些其他的函数。
  • 由于这似乎是家庭作业,我不会给你一个完整的答案,但我会给你一个提示:List.contains " " 不是检查空字符串,而是检查包含单一空间。如果你想检查空字符串,你应该写List.contains ""
  • Falsifiable, after 1 test (1 shrink) (StdGen (1956008827,296526183)): Original: [""; "\012"] (At least one control character has been escaped as a char code, e.g. \023) Shrunk: [""] 。这是否意味着因为“”这个我的测试返回错误?我说应该是假的,如果是假的不行吗?

标签: functional-programming f# fscheck


【解决方案1】:

我认为您还不太了解 FsCheck。当您执行Check.Verbose someFunction 时,FsCheck 会为您的函数生成一堆随机输入,如果函数曾经返回 false,则会失败。这个想法是,您传递给Check.Verbose 的函数应该是一个始终为真的属性无论输入是什么。例如,如果您反转一个列表两次,那么无论原始列表是什么,它都应该返回原始列表。该属性通常表示如下:

let revTwiceIsSameList (lst : int list) =
    List.rev (List.rev lst) = lst

Check.Verbose revTwiceIsSameList  // This will pass

另一方面,您的函数是一个很好、有用的函数,可以检查列表在您的数据模型中是否格式正确...但它不是 FsCheck 意义上的 属性使用术语(即,一个函数应该总是返回真无论输入是什么)。要创建 FsCheck 样式的属性,您需要编写一个大致如下所示的函数:

let verifyMyFunc (input : string list) =
    if (input is well-formed) then  // TODO: Figure out how to check that
        myFunc input = true
    else
        myFunc input = false

Check.Verbose verifyMyFunc

(请注意,我将您的函数命名为 myFunc 而不是 list,因为作为一般规则,您永远不应该将函数命名为 list。名称 list数据类型(例如,string listint list),如果您将函数命名为 list,稍后当同一个名称有两种不同的含义时,您只会感到困惑。)

现在,这里的问题是:您如何编写我的verifyMyFunc 示例中的“输入格式正确”部分?你不能只使用你的函数来检查它,因为那会测试你的函数本身,这不是一个有用的测试。 (测试本质上会变成“myFunc input = myFunc input”,即使你的函数有错误,它也总是返回 true ——当然,除非你的函数返回随机输入)。因此,您必须编写另一个函数来检查输入是否格式正确,这里的问题是您编写的函数是检查格式正确的输入的最佳、最正确的方法。如果你写了另一个函数来检查,它最终会归结为not (List.contains "" || List.contains null),同样,你实际上是在检查你的函数。

在这种特定情况下,我不认为 FsCheck 是适合这项工作的工具,因为您的功能是如此简单。这是一项家庭作业吗,您的老师要求您使用 FsCheck?或者您是否尝试自己学习 FsCheck,并使用此练习自学 FsCheck?如果是前者,那么我建议您将您的教练指向这个问题,看看他对我的回答有何看法。如果是后者,那么我建议找一些稍微复杂一点的函数来学习 FsCheck。这里一个有用的功能是您可以找到一些应该始终为真的属性,例如在List.rev 示例中(反转列表两次应该恢复原始列表,因此这是一个有用的测试属性)。或者,如果您在查找始终为真的属性时遇到问题,至少找到一个您可以通过至少两种不同方式实现的函数,以便您可以使用 FsCheck 检查两种实现对于任何给定输入是否返回相同的结果。

【讨论】:

    【解决方案2】:

    添加到@rmunn 的出色答案:

    如果您想测试 myFunc(是的,我还重命名了您的 list 函数),您可以通过创建一些您已经知道答案的固定案例来做到这一点,例如:

    let myFunc p = if List.contains " " p || List.contains null p then false else true
    
    let tests =
        testList "myFunc" [
            testCase "empty list"    <| fun()-> "empty" |> Expect.isTrue  (myFunc [      ])
            testCase "nonempty list" <| fun()-> "hi"    |> Expect.isTrue  (myFunc [ "hi" ])
            testCase "null case"     <| fun()-> "null"  |> Expect.isFalse (myFunc [ null ])
            testCase "empty string"  <| fun()-> "\"\""  |> Expect.isFalse (myFunc [ ""   ])
        ]
    
    Tests.runTests config tests
    

    这里我使用了一个名为 Expecto 的测试库。

    如果你运行它,你会看到其中一个测试失败:

    失败了! myFunc/空字符串: “”。实际值为真,但预期为假。

    因为你原来的函数有错误;它检查空格 " " 而不是空字符串 ""

    修复后所有测试通过:

    4 个测试在 00:00:00.0105346 中为 myFunc 运行 – 4 个通过,0 个被忽略,0 个 失败,0 错误。成功!

    此时您只检查了 4 个简单而明显的案例,每个案例都有零个或一个元素。很多时候,当输入更复杂的数据时,函数会失败。问题是您可以添加多少测试用例?可能性实际上是无限的!

    FsCheck

    这就是 FsCheck 可以为您提供帮助的地方。使用 FsCheck,您可以检查应该始终为真的属性(或规则)。想出好东西来测试和认可需要一点点创造力,有时这并不容易。

    在您的情况下,我们可以测试连接。规则是这样的:

    • 如果连接两个列表,则应用于连接的 MyFunc 的结果应为 true(如果两个列表格式正确)和 false(如果其中任何一个列表格式错误)。

    你可以用这种方式把它表达为一个函数:

    let myFuncConcatenation l1 l2 = myFunc (l1 @ l2) = (myFunc l1 && myFunc l2)
    

    l1 @ l2 是两个列表的串联。

    现在如果你调用 FsCheck:

    FsCheck.Verbose myFuncConcatenation
    

    它尝试了 100 种不同的组合,试图让它失败,但最终它给了你成功:

    0:
    ["X"]
    ["^"; ""]
    1:
    ["C"; ""; "M"]
    []
    2:
    [""; ""; ""]
    [""; null; ""; ""]
    3:
    ...
    Ok, passed 100 tests.
    

    这并不一定意味着您的函数是正确的,仍然可能存在 FsCheck 没有尝试的失败组合,或者它可能以不同的方式出错。但这很好地表明它在连接属性方面是正确的。

    使用 FsCheck 测试连接属性实际上允许我们使用不同的值调用 myFunc 300 次,并证明它没有崩溃或返回意外值。

    FsCheck 不是逐个测试,而是对其进行补充:

    请注意,如果您在有错误的原始函数上运行 FsCheck.Verbose myFuncConcatenation,它仍然会通过。原因是该错误与串联属性无关。这意味着您应该始终对最重要的情况进行逐案测试,并且可以使用 FsCheck 对其进行补充以测试其他情况。

    您可以检查以下其他属性,这些属性分别测试两个错误条件:

    let myFuncHasNulls l = if List.contains null l then myFunc l = false else true
    let myFuncHasEmpty l = if List.contains ""   l then myFunc l = false else true
    
    Check.Quick myFuncHasNulls
    Check.Quick myFuncHasEmpty
    
    // Ok, passed 100 tests.
    // Ok, passed 100 tests.
    

    【讨论】:

      猜你喜欢
      • 2018-02-03
      • 1970-01-01
      • 2018-06-12
      • 1970-01-01
      • 1970-01-01
      • 2021-07-30
      • 1970-01-01
      • 2016-04-04
      相关资源
      最近更新 更多