工程

使用 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 文档