生成负面 Scalacheck 场景的模式:使用基于 属性 的测试来测试 Scala 中的验证逻辑
Pattern for generating negative Scalacheck scenarios: Using property based testing to test validation logic in Scala
我们正在寻找一种可行的设计模式来构建 Scalacheck Gen
(生成器),它可以产生正面和负面的测试场景。这将使我们能够 运行 forAll
测试以验证功能(正面案例),并通过在所有 invalid[= 上失败来验证我们的案例 class 验证是否正常工作75=] 数据组合。
制作一个简单的、参数化的 Gen
一次性完成此操作非常容易。例如:
def idGen(valid: Boolean = true): Gen[String] = Gen.oneOf(ID.values.toList).map(s => if (valid) s else Gen.oneOf(simpleRandomCode(4), "").sample.get)
通过以上,我可以得到一个有效或无效的 ID 用于测试目的。有效的,我用来确保业务逻辑成功。无效的,我用来确保我们的验证逻辑拒绝 class.
的情况
好的,所以 - 问题是,在大范围内,这变得非常笨重。假设我有一个包含 100 个不同元素的数据容器。生成一个 "good" 很容易。但是现在,我想生成一个"bad",而且:
我想为每个数据元素生成一个错误的数据元素,其中一个数据元素是错误的(因此至少,至少 100 个错误实例,测试每个无效参数是否被验证逻辑捕获).
我希望能够覆盖 特定的 元素,例如输入错误的 ID 或错误的 "foobar." 不管是什么。
我们可以寻求灵感的一种模式是 apply
和 copy
,这使我们能够在指定重写值的同时轻松组合新对象。例如:
val f = Foo("a", "b") // f: Foo = Foo(a,b)
val t = Foo.unapply(f) // t: Option[(String, String)] = Some((a,b))
Foo(t.get._1, "c") // res0: Foo = Foo(a,c)
上面我们看到了从另一个对象的模板创建变异对象的基本思想。这在 Scala 中更容易表达为:
val f = someFoo copy(b = "c")
以此为灵感,我们可以思考我们的 objective。需要考虑的几点:
首先,我们可以为数据元素和生成的值定义一个key/values的映射或容器。这可以用来代替元组来支持命名值突变。
给定一个包含 key/value 对的容器,我们可以很容易地随机 select 一对(或多对)并更改一个值。这支持 objective 生成一个数据集,其中一个值被更改以创建失败。
给定这样一个容器,我们可以很容易地从无效的值集合中创建一个新对象(使用 apply()
或其他一些技术)。
或者,也许我们可以开发一个使用元组的模式,然后只是 apply()
它,有点像 copy
方法,只要我们仍然可以随机更改一个或多个值。
我们或许可以探索开发一个可重复使用的模式来做这样的事情:
def thingGen(invalidValueCount: Int): Gen[Thing] = ???
def someTest = forAll(thingGen) { v => invalidV = v.invalidate(1); validate(invalidV) must beFalse }
在上面的代码中,我们有一个生成器thingGen
,returns(有效)Things
。然后对于返回的所有实例,我们调用一个通用方法 invalidate(count: Int)
,它将随机使 count
值无效,返回一个无效对象。然后我们可以使用它来确定我们的验证逻辑是否正常工作。
这将需要定义一个 invalidate()
函数,给定一个参数(按名称或按位置)然后可以用已知错误的值替换已识别的参数。这意味着对特定值有一个 "anti-generator",例如,如果一个 ID 必须是 3 个字符,那么它知道创建一个长度不超过 3 个字符的字符串。
当然要使已知的单个参数无效(将错误数据注入测试条件)我们可以简单地使用复制方法:
def thingGen(invalidValueCount: Int): Gen[Thing] = ???
def someTest = forAll(thingGen) { v => v2 = v copy(id = "xxx"); validate(v2) must beFalse }
这是我迄今为止的想法的总和。我是不是找错树了?是否有处理此类测试的良好模式?关于如何最好地解决这个测试我们的验证逻辑的问题,有什么评论或建议吗?
我们可以组合一个有效实例和一组无效字段(这样 每个 字段,如果被复制,将导致验证失败)使用 shapeless图书馆.
Shapeless 允许您将 class 表示为仍然是强类型并支持某些 high-level 操作的 key-value 对的列表,并将此表示转换回原始表示class.
在下面的示例中,我将为提供的每个字段提供一个无效实例
import shapeless._, record._
import shapeless.labelled.FieldType
import shapeless.ops.record.Updater
详细介绍
假设我们有一个数据 class 和它的一个有效实例(我们只需要一个,所以它可以被硬编码)
case class User(id: String, name: String, about: String, age: Int) {
def isValid = id.length == 3 && name.nonEmpty && age >= 0
}
val someValidUser = User("oo7", "Frank", "A good guy", 42)
assert(someValidUser.isValid)
然后我们可以定义一个 class 用于无效值:
case class BogusUserFields(name: String, id: String, age: Int)
val bogusData = BogusUserFields("", "1234", -5)
可以使用 ScalaCheck 提供此类 classes 的实例。编写一个所有字段都会导致失败的生成器要容易得多。字段顺序无关紧要,但它们的名称和类型很重要。在这里,我们从 User
组字段中排除了 about
,这样我们就可以按照您的要求进行操作(仅提供您要测试的部分字段)
然后我们使用LabelledGeneric[T]
将User
和BogusUserFields
转换为它们对应的记录值(稍后我们将User
转换回来)
val userLG = LabelledGeneric[User]
val bogusLG = LabelledGeneric[BogusUserFields]
val validUserRecord = userLG.to(someValidUser)
val bogusRecord = bogusLG.to(bogusData)
记录是 key-value 对的列表,所以我们可以使用 head
来获得单个映射,并且 +
运算符支持向另一条记录添加/替换字段。让我们一次将每个无效字段选入我们的用户。另外,这是转换回来的动作:
val invalidUser1 = userLG.from(validUserRecord + bogusRecord.head)// invalid name
val invalidUser2 = userLG.from(validUserRecord + bogusRecord.tail.head)// invalid ID
val invalidUser3 = userLG.from(validUserRecord + bogusRecord.tail.tail.head) // invalid age
assert(List(invalidUser1, invalidUser2, invalidUser3).forall(!_.isValid))
因为我们基本上是对 bogusRecord
中的每个 key-value 对应用相同的函数 (validUserRecord + _
),所以我们也可以使用 map
运算符,除了我们使用它具有不寻常的 - 多态 - 功能。我们也可以轻松地将其转换为 List
,因为现在每个元素都属于同一类型。
object polymerge extends Poly1 {
implicit def caseField[K, V](implicit upd: Updater[userLG.Repr, FieldType[K, V]]) =
at[FieldType[K, V]](upd(validUserRecord, _))
}
val allInvalidUsers = bogusRecord.map(polymerge).toList.map(userLG.from)
assert(allInvalidUsers == List(invalidUser1, invalidUser2, invalidUser3))
概括并删除所有样板文件
现在的重点是我们可以将其概括为适用于任意两个任意 classes。所有关系和操作的编码都有点麻烦,我花了一些时间才弄清楚所有 implicit not found
错误,所以我将跳过细节。
class Picks[A, AR <: HList](defaults: A)(implicit lgA: LabelledGeneric.Aux[A, AR]) {
private val defaultsRec = lgA.to(defaults)
object mergeIntoTemplate extends Poly1 {
implicit def caseField[K, V](implicit upd: Updater[AR, FieldType[K, V]]) =
at[FieldType[K, V]](upd(defaultsRec, _))
}
def from[B, BR <: HList, MR <: HList, F <: Poly](options: B)
(implicit
optionsLG: LabelledGeneric.Aux[B, BR],
mapper: ops.hlist.Mapper.Aux[mergeIntoTemplate.type, BR, MR],
toList: ops.hlist.ToTraversable.Aux[MR, List, AR]
) = {
optionsLG.to(options).map(mergeIntoTemplate).toList.map(lgA.from)
}
}
所以,它正在运行:
val cp = new Picks(someValidUser)
assert(cp.from(bogusData) == allInvalidUsers)
不幸的是,您不能写 new Picks(someValidUser).from(bogusData)
,因为 mapper
的隐式需要一个稳定的标识符。另一方面,cp
实例可以与其他类型重用:
case class BogusName(name: String)
assert(cp.from(BogusName("")).head == someValidUser.copy(name = ""))
现在它适用于所有类型!并且伪造数据必须是 class 字段的任何子集,因此它甚至可以用于 class 本身
case class Address(country: String, city: String, line_1: String, line_2: String) {
def isValid = Seq(country, city, line_1, line_2).forall(_.nonEmpty)
}
val acp = new Picks(Address("Test country", "Test city", "Test line 1", "Test line 2"))
val invalidAddresses = acp.from(Address("", "", "", ""))
assert(invalidAddresses.forall(!_.isValid))
您可以在 ScalaFiddle
查看代码 运行
我们正在寻找一种可行的设计模式来构建 Scalacheck Gen
(生成器),它可以产生正面和负面的测试场景。这将使我们能够 运行 forAll
测试以验证功能(正面案例),并通过在所有 invalid[= 上失败来验证我们的案例 class 验证是否正常工作75=] 数据组合。
制作一个简单的、参数化的 Gen
一次性完成此操作非常容易。例如:
def idGen(valid: Boolean = true): Gen[String] = Gen.oneOf(ID.values.toList).map(s => if (valid) s else Gen.oneOf(simpleRandomCode(4), "").sample.get)
通过以上,我可以得到一个有效或无效的 ID 用于测试目的。有效的,我用来确保业务逻辑成功。无效的,我用来确保我们的验证逻辑拒绝 class.
的情况好的,所以 - 问题是,在大范围内,这变得非常笨重。假设我有一个包含 100 个不同元素的数据容器。生成一个 "good" 很容易。但是现在,我想生成一个"bad",而且:
我想为每个数据元素生成一个错误的数据元素,其中一个数据元素是错误的(因此至少,至少 100 个错误实例,测试每个无效参数是否被验证逻辑捕获).
我希望能够覆盖 特定的 元素,例如输入错误的 ID 或错误的 "foobar." 不管是什么。
我们可以寻求灵感的一种模式是 apply
和 copy
,这使我们能够在指定重写值的同时轻松组合新对象。例如:
val f = Foo("a", "b") // f: Foo = Foo(a,b)
val t = Foo.unapply(f) // t: Option[(String, String)] = Some((a,b))
Foo(t.get._1, "c") // res0: Foo = Foo(a,c)
上面我们看到了从另一个对象的模板创建变异对象的基本思想。这在 Scala 中更容易表达为:
val f = someFoo copy(b = "c")
以此为灵感,我们可以思考我们的 objective。需要考虑的几点:
首先,我们可以为数据元素和生成的值定义一个key/values的映射或容器。这可以用来代替元组来支持命名值突变。
给定一个包含 key/value 对的容器,我们可以很容易地随机 select 一对(或多对)并更改一个值。这支持 objective 生成一个数据集,其中一个值被更改以创建失败。
给定这样一个容器,我们可以很容易地从无效的值集合中创建一个新对象(使用
apply()
或其他一些技术)。或者,也许我们可以开发一个使用元组的模式,然后只是
apply()
它,有点像copy
方法,只要我们仍然可以随机更改一个或多个值。
我们或许可以探索开发一个可重复使用的模式来做这样的事情:
def thingGen(invalidValueCount: Int): Gen[Thing] = ???
def someTest = forAll(thingGen) { v => invalidV = v.invalidate(1); validate(invalidV) must beFalse }
在上面的代码中,我们有一个生成器thingGen
,returns(有效)Things
。然后对于返回的所有实例,我们调用一个通用方法 invalidate(count: Int)
,它将随机使 count
值无效,返回一个无效对象。然后我们可以使用它来确定我们的验证逻辑是否正常工作。
这将需要定义一个 invalidate()
函数,给定一个参数(按名称或按位置)然后可以用已知错误的值替换已识别的参数。这意味着对特定值有一个 "anti-generator",例如,如果一个 ID 必须是 3 个字符,那么它知道创建一个长度不超过 3 个字符的字符串。
当然要使已知的单个参数无效(将错误数据注入测试条件)我们可以简单地使用复制方法:
def thingGen(invalidValueCount: Int): Gen[Thing] = ???
def someTest = forAll(thingGen) { v => v2 = v copy(id = "xxx"); validate(v2) must beFalse }
这是我迄今为止的想法的总和。我是不是找错树了?是否有处理此类测试的良好模式?关于如何最好地解决这个测试我们的验证逻辑的问题,有什么评论或建议吗?
我们可以组合一个有效实例和一组无效字段(这样 每个 字段,如果被复制,将导致验证失败)使用 shapeless图书馆.
Shapeless 允许您将 class 表示为仍然是强类型并支持某些 high-level 操作的 key-value 对的列表,并将此表示转换回原始表示class.
在下面的示例中,我将为提供的每个字段提供一个无效实例
import shapeless._, record._
import shapeless.labelled.FieldType
import shapeless.ops.record.Updater
详细介绍
假设我们有一个数据 class 和它的一个有效实例(我们只需要一个,所以它可以被硬编码)
case class User(id: String, name: String, about: String, age: Int) {
def isValid = id.length == 3 && name.nonEmpty && age >= 0
}
val someValidUser = User("oo7", "Frank", "A good guy", 42)
assert(someValidUser.isValid)
然后我们可以定义一个 class 用于无效值:
case class BogusUserFields(name: String, id: String, age: Int)
val bogusData = BogusUserFields("", "1234", -5)
可以使用 ScalaCheck 提供此类 classes 的实例。编写一个所有字段都会导致失败的生成器要容易得多。字段顺序无关紧要,但它们的名称和类型很重要。在这里,我们从 User
组字段中排除了 about
,这样我们就可以按照您的要求进行操作(仅提供您要测试的部分字段)
然后我们使用LabelledGeneric[T]
将User
和BogusUserFields
转换为它们对应的记录值(稍后我们将User
转换回来)
val userLG = LabelledGeneric[User]
val bogusLG = LabelledGeneric[BogusUserFields]
val validUserRecord = userLG.to(someValidUser)
val bogusRecord = bogusLG.to(bogusData)
记录是 key-value 对的列表,所以我们可以使用 head
来获得单个映射,并且 +
运算符支持向另一条记录添加/替换字段。让我们一次将每个无效字段选入我们的用户。另外,这是转换回来的动作:
val invalidUser1 = userLG.from(validUserRecord + bogusRecord.head)// invalid name
val invalidUser2 = userLG.from(validUserRecord + bogusRecord.tail.head)// invalid ID
val invalidUser3 = userLG.from(validUserRecord + bogusRecord.tail.tail.head) // invalid age
assert(List(invalidUser1, invalidUser2, invalidUser3).forall(!_.isValid))
因为我们基本上是对 bogusRecord
中的每个 key-value 对应用相同的函数 (validUserRecord + _
),所以我们也可以使用 map
运算符,除了我们使用它具有不寻常的 - 多态 - 功能。我们也可以轻松地将其转换为 List
,因为现在每个元素都属于同一类型。
object polymerge extends Poly1 {
implicit def caseField[K, V](implicit upd: Updater[userLG.Repr, FieldType[K, V]]) =
at[FieldType[K, V]](upd(validUserRecord, _))
}
val allInvalidUsers = bogusRecord.map(polymerge).toList.map(userLG.from)
assert(allInvalidUsers == List(invalidUser1, invalidUser2, invalidUser3))
概括并删除所有样板文件
现在的重点是我们可以将其概括为适用于任意两个任意 classes。所有关系和操作的编码都有点麻烦,我花了一些时间才弄清楚所有 implicit not found
错误,所以我将跳过细节。
class Picks[A, AR <: HList](defaults: A)(implicit lgA: LabelledGeneric.Aux[A, AR]) {
private val defaultsRec = lgA.to(defaults)
object mergeIntoTemplate extends Poly1 {
implicit def caseField[K, V](implicit upd: Updater[AR, FieldType[K, V]]) =
at[FieldType[K, V]](upd(defaultsRec, _))
}
def from[B, BR <: HList, MR <: HList, F <: Poly](options: B)
(implicit
optionsLG: LabelledGeneric.Aux[B, BR],
mapper: ops.hlist.Mapper.Aux[mergeIntoTemplate.type, BR, MR],
toList: ops.hlist.ToTraversable.Aux[MR, List, AR]
) = {
optionsLG.to(options).map(mergeIntoTemplate).toList.map(lgA.from)
}
}
所以,它正在运行:
val cp = new Picks(someValidUser)
assert(cp.from(bogusData) == allInvalidUsers)
不幸的是,您不能写 new Picks(someValidUser).from(bogusData)
,因为 mapper
的隐式需要一个稳定的标识符。另一方面,cp
实例可以与其他类型重用:
case class BogusName(name: String)
assert(cp.from(BogusName("")).head == someValidUser.copy(name = ""))
现在它适用于所有类型!并且伪造数据必须是 class 字段的任何子集,因此它甚至可以用于 class 本身
case class Address(country: String, city: String, line_1: String, line_2: String) {
def isValid = Seq(country, city, line_1, line_2).forall(_.nonEmpty)
}
val acp = new Picks(Address("Test country", "Test city", "Test line 1", "Test line 2"))
val invalidAddresses = acp.from(Address("", "", "", ""))
assert(invalidAddresses.forall(!_.isValid))
您可以在 ScalaFiddle
查看代码 运行