如何正确通过FsCheck测试

时间:2018-11-22 13:15:38

标签: functional-programming f# fscheck

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

我具有检查列表格式是否正确的功能。该列表不应包含空字符串和null。我没有得到我所缺少的东西,因为Check.Verbose list返回了可伪造的输出。

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

2 个答案:

答案 0 :(得分:3)

我认为您还不太了解FsCheck。当您执行Check.Verbose someFunction时,FsCheck会为您的函数生成一堆随机输入,如果函数 ever返回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。这里有用的功能是可以在其中找到一些应始终为true的属性,例如在List.rev示例中(两次反转列表应还原原始列表,因此这是一个有用的属性进行测试)。或者,如果您在查找始终为true的属性时遇到麻烦,请至少找到可以用至少两种不同方式实现的函数,以便可以使用FsCheck来检查两个实现对于任何给定输入是否返回相同的结果。

答案 1 :(得分: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个测试针对myFunc在00:00:00.0105346中运行–已通过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种不同的组合尝试使其失败,但最终它给了您OK:

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.