使用 Go-Lookslike 测试数据形状
我很荣幸向大家介绍我们在 Elastic 开发的一种新的开源 Go 测试/架构验证库。它的名字叫做 Lookslike。Lookslike 允许您以类似于 JSON 架构的方式与 Golang 数据结构形状进行匹配,但它比 JSON 更强大,与 Go 更为相似。它有很多在任何现有 Go 测试库中都不具备的功能。
让我们通过一个示例来直接看看它的功能:
// This library lets us check that a data-structure either similar to, or exactly like a certain schema.
// For example we could test whether a pet is a dog or a cat, using the code below.
// A dog named rover
rover := map[string]interface{}{"name": "rover", "barks": "often", "fur_length": "long"}
// A cat named pounce
pounce := map[string]interface{}{"name": "pounce", "meows": "often", "fur_length": "short"}
// Here we define a validator
// Here we define a dog as an item that has:
// 1. a "name" key that is any non-empty string, using the builtin `IsNonEmptyString` definition
// 2. a "fur_length" key that is either "long" or "short" as a value
// 3. a "bars" key that has either "often", or "rarely" as a value
// 4. we further define this as a strict matcher, meaning that if any keys other than those
// listed are present those should be considered errors
dogValidator := lookslike.Strict(lookslike.MustCompile(map[string]interface{}{
"name": isdef.IsNonEmptyString,
"fur_length": isdef.IsAny(isdef.IsEqual("long"), isdef.IsEqual("short")),
"barks": isdef.IsAny(isdef.IsEqual("often"), isdef.IsEqual("rarely")),
}))
result := dogValidator(rover)
fmt.Printf("Checked rover, validation status %t, errors: %v\n", result.Valid, result.Errors())
result = dogValidator(pounce)
fmt.Printf("Checked pounce, validation status %t, errors: %v\n", result.Valid, result.Errors())
运行上面的代码将显示如下结果。
Checked rover, validation status true, errors: []
Checked pounce, validation status false, errors: [@Path 'barks': expected this key to be present @Path 'meows': unexpected field encountered during strict validation]
在这里,我们可以看到狗“rover”按预期那样匹配了,但猫“pounce”未按预期匹配,产生了两个错误。一个错误是未定义“barks”键;另一个错误是意外出现一个多余的键“meows”。
由于 Lookslike 通常是用于测试上下文中的,因此,我们开发了 testslike.Test
帮助器,它可以生成格式良好的测试输出。您只需将上例中的最后几行改为下面显示的内容即可。
testslike.Test(t, dogValidator, rover)
testslike.Test(t, dogValidator, pounce)
复合
Lookslike 中的一个关键概念就是能够合并验证器。假设我们需要单独的 cat 和 dog 验证器,但又不想在每个验证器中重新定义像“name”和“fur_length”这样的通用字段。让我们来看看下面的示例是如何实现的。
pets := []map[string]interface{}{
{"name": "rover", "barks": "often", "fur_length": "long"},
{"name": "lucky", "barks": "rarely", "fur_length": "short"},
{"name": "pounce", "meows": "often", "fur_length": "short"},
{"name": "peanut", "meows": "rarely", "fur_length": "long"},
}
// We can see that all pets have the "fur_length" property, but that only cats meow, and dogs bark.
// We can concisely encode this in lookslike using lookslike.Compose.
// We can also see that both "meows" and "barks" contain the same enums of values.
// We'll start by creating a composed IsDef using the IsAny composition, which creates a new IsDef that is
// a logical 'or' of its IsDef arguments
isFrequency := isdef.IsAny(isdef.IsEqual("often"), isdef.IsEqual("rarely"))
petValidator := MustCompile(map[string]interface{}{
"name": isdef.IsNonEmptyString,
"fur_length": isdef.IsAny(isdef.IsEqual("long"), isdef.IsEqual("short")),
})
dogValidator := Compose(
petValidator,
MustCompile(map[string]interface{}{"barks": isFrequency}),
)
catValidator := Compose(
petValidator,
MustCompile(map[string]interface{}{"meows": isFrequency}),
)
for _, pet := range pets {
var petType string
if dogValidator(pet).Valid {
petType = "dog"
} else if catValidator(pet).Valid {
petType = "cat"
}
fmt.Printf("%s is a %s\n", pet["name"], petType)
}
// Output:
// rover is a dog
// lucky is a dog
// pounce is a cat
// peanut is a cat
为什么构建 Lookslike
Lookslike 源自 Elastic 的 Heartbeat项目。Heartbeat 是运行时间解决方案后面的代理,它会 ping 端点,然后报告端点是否在正常运行。Heartbeat 的最终输出是 Elasticsearch 文档,在 Golang 代码库中以“map[string]interface{}”类型表示。测试这些输出文档是创建这个库的需求所在,尽管现在它在 Beats 代码库的其他地方使用。
我们面临的挑战是:
- 有些字段的数据不需要精确匹配,例如“monitor.duration”,它用于计算执行所花的时间。它可能会在每次运行中产生不同的结果。我们需要一种方法来松散地匹配数据
- 在任何给定的测试中,很多字段与其他测试共享,只有少数字段是不同的。我们希望能够通过编写不同的字段定义来减少代码重复。
- 我们希望良好的测试输出能够针对各个字段失败分别显示一个错误,因此需要“testslike”测试帮助器。
鉴于这些挑战,我们做出了以下设计决策:
- 我们希望架构灵活,并且希望开发人员能够轻松地创建新的匹配器。
- 我们希望所有架构都是可组合和可嵌套的,这样,如果您在一个文档中嵌套了另一个文档,就可以合并架构而无需复制一堆代码了。
- 我们需要一个好的测试帮助器让测试失败易于读取。
主要类型
Lookslike 的体系结构解析两个主要类型:“Validator”和“IsDef”。“Validator”是编译给定架构的结果。它是一个接受任意数据结构并返回结果的函数。“IsDef”是用于匹配单个字段的类型。您可能想知道为什么它们之间有区别。事实上,我们将来可能会合并这两个类型。但是,主要原因是“IsDef”在文档结构中获取了关于其位置的额外参数,允许它基于该上下文执行高级验证。“Validator”函数不接收额外的上下文,但更便于用户执行(它们只接受“interface{}”并对其进行验证)。
有关编写自定义“IsDefs”的示例,只需参阅源文件即可。您可以将新的“IsDef”添加到自己的源来进行扩展。
现场示例
我们在 Beats 中大量使用了 Lookslike。通过此 github 搜索,可以找到大量用法示例。
我们需要您的帮助!
如果您对 Lookslike 感兴趣,请在 repo上提交拉取请求!我们可以特别使用一组更全面的“IsDef”。
了解更多
我们已经在努力来阐述 Lookslike 了。您可以在 godoc.org 上浏览 Lookslike 文档。