基于属性的测试会让你重复代码吗?

Chi*_*rlo 3 unit-testing scalatest scalacheck property-based-testing

我正在尝试用基于属性的测试 (PBT) 替换一些旧的单元测试,具体地用scalascalatest - scalacheck但我认为这个问题更普遍。简化的情况是,如果我有要测试的方法:

 def upcaseReverse(s:String) = s.toUpperCase.reverse
Run Code Online (Sandbox Code Playgroud)

通常,我会编写单元测试,例如:

assertEquals("GNIRTS", upcaseReverse("string"))
assertEquals("", upcaseReverse(""))
// ... corner cases I could think of
Run Code Online (Sandbox Code Playgroud)

所以,对于每个测试,我写出我期望的输出,没问题。现在,使用 PBT,它会像:

property("strings are reversed and upper-cased") {
 forAll { (s: String) =>
   assert ( upcaseReverse(s) == ???) //this is the problem right here!
 }
}
Run Code Online (Sandbox Code Playgroud)

当我尝试编写一个对所有String输入都适用的测试时,我发现自己不得不在测试中再次编写方法的逻辑。在这种情况下,测试将如下所示:

   assert ( upcaseReverse(s) == s.toUpperCase.reverse) 
Run Code Online (Sandbox Code Playgroud)

也就是说,我必须在测试中编写实现以确保输出正确。有没有办法解决这个问题?我是否误解了 PBT,我是否应该测试其他属性,例如:

  • “字符串的长度应与原始字符串的长度相同”
  • “字符串应该包含原始的所有字符”
  • “字符串不应包含小写字符”...

这也是合理的,但听起来很做作,不太清楚。任何在 PBT 方面有更多经验的人都可以在这里解释一下吗?

编辑:按照@Eric 的消息来源,我找到了这篇文章,并且有一个我的意思的示例(在再次应用类别时):测试times( F#) 中的方法 :

type Dollar(amount:int) =
member val Amount  = amount 
member this.Add add = 
    Dollar (amount + add)
member this.Times multiplier  = 
    Dollar (amount * multiplier)
static member Create amount  = 
    Dollar amount  
Run Code Online (Sandbox Code Playgroud)

作者最终编写了一个测试,如下所示:

let ``create then times should be same as times then create`` start multiplier = 
let d0 = Dollar.Create start
let d1 = d0.Times(multiplier)
let d2 = Dollar.Create (start * multiplier)      // This ones duplicates the code of Times!
d1 = d2
Run Code Online (Sandbox Code Playgroud)

因此,为了测试一个方法,该方法的代码在测试中被复制。在这种情况下,像乘法一样微不足道,但我认为它可以外推到更复杂的情况。

Eri*_*ric 5

此演示文稿提供了一些有关您可以为代码编写的属性类型的线索,而无需复制它。

通常,考虑将要测试的方法与该类上的其他方法组合时会发生什么是有用的:

  • size
  • ++
  • reverse
  • toUpperCase
  • contains

例如:

  • upcaseReverse(y) ++ upcaseReverse(x) == upcaseReverse(x ++ y)

然后想想如果实现被破坏会破坏什么。如果出现以下情况,财产是否会失败:

  1. 大小没有保留?
  2. 不是所有的字符都是大写的?
  3. 字符串没有正确反转?

1. 实际上是由 3. 隐含的,我认为上面的属性会中断 3。但是它不会中断 2(例如,如果根本没有大写)。我们可以增强它吗?关于什么:

  • upcaseReverse(y) ++ x.reverse.toUpper == upcaseReverse(x ++ y)

我认为这个没问题,但不要相信我并运行测试!

无论如何,我希望你能明白:

  1. 与其他方法组合
  2. 查看是否存在似乎成立的等式(如演示中的“往返”或“幂等性”或“模型检查”)
  3. 检查代码错误时您的财产是否会损坏

请注意, 1. 和 2. 是由名为QuickSpec的库实现的,而 3. 是"mutation testing"

附录

关于您的编辑:该Times操作只是一个包装,*因此没有太多要测试的内容。但是,在更复杂的情况下,您可能需要检查操作:

  • 有一个unit元素
  • 是联想的
  • 是可交换的
  • 与加法是分配的

如果这些属性中的任何一个失败,这将是一个很大的惊喜。如果您将这些属性编码为任何二元关系的通用属性,T x T -> T您应该能够在各种上下文中非常轻松地重用它们(请参阅 Scalaz Monoid“定律”)。

回到您的upperCaseReverse示例,我实际上会编写 2 个单独的属性:

 "upperCaseReverse must uppercase the string" >> forAll { s: String =>
    upperCaseReverse(s).forall(_.isUpper)
 }

 "upperCaseReverse reverses the string regardless of case" >> forAll { s: String =>
    upperCaseReverse(s).toLowerCase === s.reverse.toLowerCase
 }
Run Code Online (Sandbox Code Playgroud)

这不会复制代码并说明 2 个不同的事情,如果您的代码错误,这些事情可能会中断。

总之,我以前和你有同样的问题,对此感到非常沮丧,但过了一段时间我发现越来越多的情况下我没有在属性中复制我的代码,尤其是当我开始考虑

  • 将测试函数与其他函数结合(.isUpper在第一个属性中)
  • 将测试函数与更简单的计算“模型”进行比较(第二个属性中的“无论大小写都反向”)