难以思考FsCheck的属性

Ben*_*jol 13 f# unit-testing 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)
Run Code Online (Sandbox Code Playgroud)

Kur*_*out 14

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

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

作为一般建议,尽量不要做太大的交易.对于sndBigger来说,一个好的财产将是:

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

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

也许这个链接 (由Pex团队提供)也提供了一些想法.


Tom*_*cek 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
Run Code Online (Sandbox Code Playgroud)

编辑:此规则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))
Run Code Online (Sandbox Code Playgroud)

关于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
Run Code Online (Sandbox Code Playgroud)

因为splitOn,我们可以测试类似的东西 - 如果你连接所有返回的列表,它应该给出原始列表(这不会验证分裂行为,但它开始是一件好事 - 它至少保证没有元素将迷路了.

let prop_splitOnEq f x = 
  x |> splitOn f |> List.concat = x
Run Code Online (Sandbox Code Playgroud)

我不确定FsCheck是否可以处理这个(!),因为该属性将函数作为参数(因此它需要生成"随机函数").如果这不起作用,您需要提供一些具有一些手写功能的特定属性f.接下来,实现f对分割列表中的所有相邻对返回true 的检查(如kvb所示)实际上并不困难:

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

可能你可以检查的唯一最后一件事就是当你给它一个列表中的最后一个元素和下一个列表中的第一个元素时f返回false.以下内容尚未完全完成,但它显示了要走的路:

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

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


kvb*_*kvb 7

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

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

  • 检查不应该更复杂 - 这确实违背了测试的目的.但根据我的经验,它们可能同样复杂.随着时间的推移,您的实现可能会变得更加复杂(例如优化),而属性/规范通常或多或少保持不变.因此,虽然现在可能没有意义,但您可能会对以后的属性感到满意. (3认同)