如何正确通过 FsCheck 测试

How to pass FsCheck Test Correctly

let list p = if List.contains " " p || List.contains null p then false else true

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

我该如何解决这个问题?

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

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

Check.Verbose revTwiceIsSameList  // This will pass

另一方面,您的函数是一个很好、有用的函数,用于检查列表在您的数据模型中是否格式正确...但它不是 属性 在 FsCheck 使用术语的意义上(也就是说,一个函数应该 always return true 无论输入是什么).要制作 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 示例的 "input is well-formed" 部分?你不能只使用你的函数来检查它,因为那会测试你的函数自身,这不是一个有用的测试。 (测试基本上会变成 "myFunc input = myFunc input",即使您的函数中有错误,它也总是 return 为真——当然,除非您的函数 return 随机输入)。所以你必须编写另一个函数来检查输入是否格式正确,这里的问题是你编写的函数是检查格式正确的最佳、最正确的方法。如果你写了另一个函数来检查,它最终会归结为 not (List.contains "" || List.contains null),而且,你实际上是在检查你的函数本身。

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

添加到@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.

的测试库

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

Failed! myFunc/empty string: "". Actual value was true but had expected it to be false.

因为你原来的函数有bug;它检查 space " " 而不是空字符串 "".

修复后所有测试都通过:

4 tests run in 00:00:00.0105346 for myFunc – 4 passed, 0 ignored, 0 failed, 0 errored. Success!

此时你只检查了 4 个简单明显的案例,每个案例有 0 个或 1 个元素。当输入更复杂的数据时,很多时候功能会失败。问题是您还可以添加多少个测试用例?无限可能!

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.