如何正确通过 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 list
或 int 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.
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 list
或 int 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.