基于属性的测试是否会使您重复代码?

时间:2015-06-19 12:43:09

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

我试图用基于属性的测试(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,我是否应该测试其他属性,例如:

  • "字符串应与原始字符串#34;
  • 具有相同的长度
  • "字符串应包含原始"
  • 的所有字符
  • "字符串不应包含小写字符" ...

这也是合理的,但听起来有点做作,不太清楚。任何在PBT方面有更多经验的人都能在这里得到一些启发吗?

编辑:关注@ Eric的来源我得到this post,这就是我的意思的一个例子(在应用类别再一次):测试(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  

作者最终编写的测试类似于:

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

因此,为了测试该方法,该方法的代码在测试中重复。在这种情况下,有些东西像乘法一样微不足道,但我认为它可以推断出更复杂的情况。

2 个答案:

答案 0 :(得分:3)

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

一般来说,考虑一下当你想要用该类上的其他方法测试方法时会发生什么是很有用的:

  • size
  • ++
  • reverse
  • toUpperCase
  • contains

例如:

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

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

  1. 尺寸未保存?
  2. 并非所有角色都是大写的?
  3. 字符串没有正确反转?
  4. 1。实际上是由3隐含的。我认为上面的属性会突破3但是它不会突破2(例如,如果根本没有大写)。我们能加强吗?怎么样:

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

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

    无论如何,我希望你明白这个想法:

    1. 用其他方法撰写
    2. 看看是否存在似乎存在的等同性(例如“圆形跳闸”或“幂等性”或“模型检查”)
    3. 检查代码错误时您的财产是否会中断
    4. 请注意,1.和2.由名为QuickSpec的库实现,而3.是"mutation testing"

      附录

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

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

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

      回到你的upperCaseReverse例子,我实际上会写两个独立的属性:

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

      这不会复制代码,并指出如果您的代码错误可能会破坏的两个不同的东西。

      总而言之,我和你之前有过同样的问题并对此感到非常沮丧但过了一段时间后我发现越来越多的案例我在属性中复制我的代码,特别是当我开始考虑

      • 将测试函数与其他函数(第一个属性中的.isUpper)相结合
      • 将测试函数与更简单的“计算模型”进行比较(第二个属性中“不管情况如何”)

答案 1 :(得分:1)

我将这个问题称为“收敛测试”,但我无法弄清楚为什么或从何处得出该术语,因此请带上一粒盐。

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

在您的情况下,代码最终基本上是相同的,只是两次编写相同的代码。有时候,这是有价值的。例如,如果您正在编写代码以使某人在重症监护病房中存活,则可以写两次以确保安全。我不会为您的谨慎而生错。

在其他情况下,测试破裂的可能性使测试捕获实际问题的好处无效。因此,即使以其他方式违反最佳做法(枚举应计算的内容,而不是编写DRY代码),我也会尝试编写比生产代码更简单的测试代码,因此不太可能失败。

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

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

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