3

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

 def upcaseReverse(s:String) = s.toUpperCase.reverse

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

assertEquals("GNIRTS", upcaseReverse("string"))
assertEquals("", upcaseReverse(""))
// ... corner cases I could think of

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

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

当我尝试编写一个适用于所有String输入的测试时,我发现自己不得不在测试中再次编写该方法的逻辑。在这种情况下,测试看起来像:

   assert ( upcaseReverse(s) == s.toUpperCase.reverse) 

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

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

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

编辑:按照@Eric的资料,我得到了这篇文章,并且有一个我的意思的例子(在再次应用类别时):测试 timesF#)中的方法:

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  

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

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

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

4

2 回答 2

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. 是“突变测试”

附录

关于您的编辑:该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
 }

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

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

  • 将测试函数与其他函数结合(.isUpper在第一个属性中)
  • 将测试函数与更简单的计算“模型”进行比较(第二个属性中的“不考虑大小写”)
于 2015-06-19T13:13:05.767 回答
1

我把这个问题称为“收敛测试”,但我不知道为什么或从哪里来,所以对它持保留态度。

对于任何测试,您都冒着测试代码的复杂性接近被测代码的复杂性的风险。

在您的情况下,代码最终基本相同,只是编写了两次相同的代码。有时这很有价值。例如,如果您正在编写代码以使重症监护中的某人保持生命,那么您可以编写两次以确保安全。我不会因为你的谨慎而责怪你。

在其他情况下,测试中断的可能性会使测试捕获实际问题的好处失效。出于这个原因,即使它在其他方面违反最佳实践(枚举应该计算的东西,而不是编写 DRY 代码)我尝试编写在某种程度上比生产代码更简单的测试代码,所以不太可能失败。

如果我找不到比测试代码更简单的方法来编写代码,那也是可维护的(阅读:“我也喜欢”),我将该测试移至“更高”级别(例如单元测试->功能测试)

我刚开始使用基于属性的测试,但据我所知,很难让它与许多单元测试一起使用。对于复杂的单元,它可以工作,但到目前为止我发现它对功能测试更有帮助。

对于功能测试,您通常可以编写函数必须满足的规则,这比编写满足规则的函数要简单得多。这对我来说很像 P vs NP 问题。您可以编写一个程序来在线性时间内验证解决方案,但所有已知的找到解决方案的程序都需要更长的时间。这似乎是一个很好的属性测试案例。

于 2020-08-24T03:54:16.087 回答