难以思考FsCheck的属性

时间:2010-03-15 09:59:19

标签: unit-testing f# fscheck property-based-testing

我设法让xUnit处理我的小样本组装。现在我想知道我是否也可以找到FsCheck。我的问题是,在为我的函数定义测试属性时,我感到很难过。

也许我只是没有一套好的样本函数,但是这些函数的测试属性是什么,例如?

//transforms [1;2;3;4] into [(1,2);(3,4)]
pairs : 'a list -> ('a * 'a) list      //'

//splits list into list of lists when predicate returns 
//  true for adjacent elements
splitOn : ('a -> 'a -> bool) -> 'a list -> 'a list list

//returns true if snd is bigger
sndBigger : ('a * 'a) -> bool (requires comparison)

3 个答案:

答案 0 :(得分:14)

已经有很多具体的答案,所以我会尝试给出一些可能会给你一些想法的一般答案。

  1. 递归函数的归纳属性。对于简单函数,这可能会重新实现递归。但是,保持简单:虽然实际的实现经常发展(例如它变为尾递归,你添加memoization,...)保持属性直截了当。 ==>属性组合器通常在这里派上用场。你的配对功能可能就是一个很好的例子。
  2. 保留模块或类型中的多个函数的属性。检查抽象数据类型时通常会出现这种情况。例如:向数组添加元素意味着数组包含该元素。这将检查Array.add和Array.contains的一致性。
  3. 往返:这适用于转换(例如解析,序列化) - 生成任意表示,序列化,反序列化,检查它是否等于原始。 您可以使用splitOn和concat执行此操作。
  4. 作为健全性检查的一般属性。寻找可能存在的众所周知的属性 - 诸如交换性,关联性,幂等性(应用两次不会改变结果),反身性等等。这里的想法更多的是运用函数 - 看看它是否做了什么真的很奇怪
  5. 作为一般建议,尽量不要做太大的交易。对于sndBigger来说,一个好的财产将是:

    让``当且仅当snd更大时才应该返回``(a:int)(b:int)=     sndBigger(a,b)= b>一个

    这可能就是实施。不要担心 - 有时一个简单的,老式的单元测试正是你需要的。无需内疚! :)

    也许this link(Pex团队)也提出了一些想法。

答案 1 :(得分:10)

我将从sndBigger开始 - 这是一个非常简单的函数,但是您可以编写一些应该保留它的属性。例如,当您反转元组中的值时会发生什么:

// Reversing values of the tuple negates the result
let swap (a, b) = (b, a)
let prop_sndBiggerSwap x = 
  sndBigger x = not (sndBigger (swap x))

// If two elements of the tuple are same, it should give 'false'
let prop_sndBiggerEq a = 
  sndBigger (a, a) = false

编辑:此规则prop_sndBiggerSwap并不总是成立(请参阅 kvb 的评论)。但是以下内容应该是正确的:

// Reversing values of the tuple negates the result
let prop_sndBiggerSwap a b = 
  if a <> b then 
    let x = (a, b)
    sndBigger x = not (sndBigger (swap x))

关于pairs功能, kvb 已经发布了一些好主意。此外,您可以检查将转换后的列表转换回元素列表会返回原始列表(当输入列表为奇数时,您需要处理这种情况 - 取决于pairs函数应该执行的操作这种情况):

let prop_pairsEq (x:_ list) = 
  if (x.Length%2 = 0) then
    x |> pairs |> List.collect (fun (a, b) -> [a; b]) = x
  else true

对于splitOn,我们可以测试类似的东西 - 如果你连接所有返回的列表,它应该给出原始列表(这不会验证分裂行为,但是开始时是一件好事 - 它至少保证不会丢失任何元素)。

let prop_splitOnEq f x = 
  x |> splitOn f |> List.concat = x

我不确定 FsCheck 是否可以处理这个(!),因为该属性将函数作为参数(因此需要生成“随机函数”)。如果这不起作用,则需要使用一些手写函数f提供一些更具体的属性。接下来,对分割列表中的所有相邻对({ kvb 建议)实施f返回true的检查实际上并不困难:

let prop_splitOnAdjacentTrue f x = 
  x |> splitOn f 
    |> List.forall (fun l -> 
         l |> Seq.pairwise 
           |> Seq.forall (fun (a, b) -> f a b))

当你从一个列表中给出最后一个元素并从下一个列表中给出第一个元素时,你可以检查的唯一最后一件事是f返回false。以下内容尚未完全完成,但它显示了可行的方法:

let prop_splitOnOtherFalse f x = 
  x |> splitOn f
    |> Seq.pairwise 
    |> Seq.forall (fun (a, b) -> lastElement a = firstElement b)

最后一个示例还显示您应该检查splitOn函数是否可以返回空列表作为返回结果列表的一部分(因为在这种情况下,您找不到第一个/最后一个元素)。

答案 2 :(得分:7)

对于某些代码(例如sndBigger),实现非常简单,任何属性都至少与原始代码一样复杂,因此通过FsCheck进行测试可能没有意义。但是,对于其他两个函数,这里有一些你可以检查的东西:

  • pairs
    • 当原始长度不能被2整除时,会发生什么?如果这是正确的行为,你可以检查抛出异常。
    • List.map fst (pairs x) = evenEntries xList.map snd (pairs x) = oddEntries x表示您可以编写的简单函数evenEntriesoddEntries
  • splitOn
    • 如果我理解你对该函数应该如何工作的描述,那么你可以检查条件,例如“对于splitOn f l的结果中的每个列表,没有两个连续的条目满足f”和“Take lists {{ 1}}来自(l1,l2)成对,splitOn f l成立“。不幸的是,这里的逻辑可能在复杂性上与实现本身相当。