如何正确通过FsCheck测试


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

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

我应该如何处理这个问题?

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

let revTwiceIsSameList (lst : int list) =
List.rev (List.rev lst) = lst
Check.Verbose revTwiceIsSameList  // This will pass

另一方面,您的函数是一个很好、有用的函数,可以检查数据模型中的列表是否形成良好。。。但从FsCheck使用术语的意义上讲,它不是属性(也就是说,无论输入是什么,都应该返回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示例中的"输入格式良好"部分?你不能只使用你的函数来检查它,因为这将是对你的函数本身的测试,这不是一个有用的测试。(测试本质上会变成"myFunc input=myFunc input",即使函数中有错误,它也会始终返回true——当然,除非函数返回随机输入)。因此,你必须编写另一个函数来检查输入是否格式正确,而这里的问题是,你编写的函数是检查输入格式正确的最佳、最正确的方法。如果你写了另一个函数来检查,它最终会归结为not (List.contains "" || List.contains null),同样,你本质上是在检查你的函数本身。

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

添加到@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/空字符串:"。实际值为真,但预期为假。

因为您的原始函数有一个错误;它检查空间CCD_ 17而不是空字符串CCD_。

修复后,所有测试都通过:

myFunc的4个测试在00:00:00.0105346运行–4个通过,0个忽略,0失败,0出错。成功

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

Fs检查

FsCheck可以在这里为您提供帮助。使用FsCheck,您可以检查应该始终为true的属性(或规则)。想出好的测试方法需要一点创造力,有时这并不容易。

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

  • 如果将两个列表连接在一起,则应用于连接的MyFunc的结果应为true(如果两个列表都格式良好)和false(如果其中任何列表格式不正确)

你可以用这种方式将其表示为一个函数:

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

CCD_ 22是两个列表的串联。

现在,如果您调用FsCheck:

FsCheck.Verbose myFuncConcatenation

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

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

这并不一定意味着你的函数是正确的,FsCheck可能没有尝试过失败的组合,也可能是错误的。但这是一个很好的指示,表明它在连接属性方面是正确的。

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

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.

最新更新