【问题标题】:Does property based testing make you duplicate code?基于属性的测试是否会使您重复代码?
【发布时间】:2015-09-05 10:54:17
【问题描述】:

我正在尝试用基于属性的测试 (PBT) 替换一些旧的单元测试,用 scalascalatest - 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 的消息来源,我找到了this post,这正是我的意思的一个例子(在再次应用类别):测试方法times in (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  

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

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

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

【问题讨论】:

  • @Eric,根据您提到的演示文稿的来源,我添加了更多信息

标签: unit-testing scalatest scalacheck property-based-testing


【解决方案1】:

This presentation 提供了一些关于可以为代码编写的属性类型的线索,而无需复制它。

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

  • 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
 }

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

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

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

【讨论】:

  • 我如何理解您的观点(以及您链接的演示文稿)是 PBT 定义了“围绕我的代码”的属性,例如:它是否通勤,它与其他方法组成......但是这些属性不要真正表达函数的结果应该是什么。如果我想要一个属性说“upcaseReverse 将反转和大写任何字符串”,除了复制代码之外,我看不到任何其他方法来测试它。您提议的属性 (upcaseReverse(y) ++ x.reverse.toUpper == upcaseReverse(x ++ y)) 已经以某种方式复制了代码,只是以不同的顺序调用方法。
  • Noooo,我有一个额外的编辑,显然丢失了。我会补充它的要点。
  • 好的,我开始明白你的意思了。我将在移植单元测试方面做更多工作,然后回到这个问题,谢谢!
【解决方案2】:

我将这个问题称为“收敛测试”,但我不知道为什么或从哪里来,所以请谨慎对待。

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

在您的情况下,代码卷起基本相同,只需两次编写相同的代码。有时候有价值。例如,如果您正在编写代码,以保持别人的重症监护,您可以写两次安全。我不会出现错误的谨慎。

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

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

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

对于功能测试,您通常可以写入 规则,函数必须满足 em>比您可以写入 满足规则的函数 em>。这对我来说感觉很像p vs np问题。在哪里可以将程序写入 validate 在线性时间的解决方案,但所有已知程序到 find 一个解决方案需要更长时间。这似乎是物业测试的精彩案例。

【讨论】:

    猜你喜欢
    • 2021-08-04
    • 2016-09-08
    • 2021-11-15
    • 1970-01-01
    • 2016-03-01
    • 1970-01-01
    • 2022-01-16
    • 2011-08-27
    • 2019-02-23
    相关资源
    最近更新 更多