总览
这是测试数据密集型代码的教程系列中的五分之五。 在第四部分中,我介绍了远程数据存储,使用共享的测试数据库,使用生产数据快照以及生成自己的测试数据。 在本教程中,我将介绍模糊测试,测试缓存,测试数据完整性,测试幂等性和数据丢失。
模糊测试
模糊测试的想法是用大量随机输入淹没系统。 您可以尝试为自己做这件事,而不是试图考虑将涵盖所有情况的输入,这些输入可能很困难和/或非常耗费人力。 它在概念上类似于随机数据生成,但此处的目的是生成随机或半随机输入,而不是持久数据。
模糊测试什么时候有用?
当意外输入导致崩溃或内存泄漏时,模糊测试特别适用于发现安全和性能问题。 但这也有助于确保及早发现所有无效输入并被系统正确拒绝。
例如,考虑以深度嵌套的JSON文档的形式出现的输入(在Web API中非常常见)。 尝试手动生成测试用例的完整列表既容易出错,也需要大量工作。 但是模糊测试是一种完美的技术。
使用模糊测试
您可以使用几个库进行模糊测试。 我最喜欢的是Google的gofuzz 。 这是一个简单的示例,该示例自动生成具有多个字段的结构的200个唯一对象,包括嵌套结构。
import (
"fmt"
"github.com/google/gofuzz"
)
func SimpleFuzzing() {
type SomeType struct {
A string
B string
C int
D struct {
E float64
}
}
f := fuzz.New()
object := SomeType{}
uniqueObjects := map[SomeType]int{}
for i := 0; i < 200; i++ {
f.Fuzz(&object)
uniqueObjects[object]++
}
fmt.Printf("Got %v unique objects.\n", len(uniqueObjects))
// Output:
// Got 200 unique objects.
}
测试缓存
几乎每个处理大量数据的复杂系统都具有一个缓存,或更可能有多个级别的分层缓存。 俗话说,计算机科学中只有两件困难的事情:命名事物,缓存失效和一次出错。
除了玩笑,管理缓存策略和实施可能会使您的数据访问复杂化,但会对数据访问成本和性能产生巨大影响。 无法从外部测试缓存,因为您的界面隐藏了数据的来源,并且缓存机制是一种实现细节。
让我们看看如何测试Songify混合数据层的缓存行为。
缓存命中和未命中
缓存会因命中/未命中性能而生存和死亡。 高速缓存的基本功能是,如果请求的数据在高速缓存中(命中)可用,则将从高速缓存而不是从主数据存储区中获取数据。 在HybridDataLayer
的原始设计中,对缓存的访问是通过私有方法完成的。
Go可见性规则使无法直接调用它们或从另一个程序包中替换它们。 为了启用缓存测试,我将那些方法更改为公共功能。 这很好,因为实际的应用程序代码通过DataLayer
接口运行,而该接口不会公开那些方法。
但是,测试代码将能够根据需要替换这些公共功能。 首先,让我们添加一种方法来访问Redis客户端,以便我们可以操纵缓存:
func (m *HybridDataLayer) GetRedis() *redis.Client {
return m.redis
}
接下来,我将把getSongByUser_DB()
方法更改为一个公共函数变量。 现在,在测试中,我可以将GetSongsByUser_DB()
变量替换为一个函数,该函数跟踪被调用的次数,然后将其转发给原始函数。 这使我们可以验证对GetSongsByUser()
的调用是否从缓存或数据库中获取了歌曲。
让我们将其分解。 首先,我们获得数据层(还清除数据库和Redis),创建用户并添加歌曲。 AddSong()
方法还填充redis。
func TestGetSongsByUser_Cache(t *testing.T) {
now := time.Now()
u := User{Name: "Gigi",
Email: "gg@gg.com",
RegisteredAt: now, LastLogin: now}
dl, err := getDataLayer()
if err != nil {
t.Error("Failed to create hybrid data layer")
}
err = dl.CreateUser(u)
if err != nil {
t.Error("Failed to create user")
}
lm, err := NewSongManager(u, dl)
if err != nil {
t.Error("NewSongManager() returned 'nil'")
}
err = lm.AddSong(testSong, nil)
if err != nil {
t.Error("AddSong() failed")
}
这是很酷的部分。 我保留了原始函数并定义了一个新的检测函数,该函数将增加本地callCount
变量(全部在闭包中)并调用原始函数。 然后,将检测功能分配给变量GetSongsByUser_DB
。 从现在开始,混合数据层对GetSongsByUser_DB()
每次调用将转到已检测函数。
callCount := 0
originalFunc := GetSongsByUser_DB
instrumentedFunc := func(m *HybridDataLayer,
email string,
songs *[]Song) (err error) {
callCount += 1
return originalFunc(m, email, songs)
}
GetSongsByUser_DB = instrumentedFunc
至此,我们准备好实际测试缓存操作了。 首先,测试调用GetSongsByUser()
的的SongManager
它转发到混合数据层。 我们应该为刚刚添加的该用户填充缓存。 因此,预期结果是将不会调用我们的检测函数,并且callCount
将保持为零。
_, err = lm.GetSongsByUser(u)
if err != nil {
t.Error("GetSongsByUser() failed")
}
// Verify the DB wasn't accessed because cache should be
// populated by AddSong()
if callCount > 0 {
t.Error(`GetSongsByUser_DB() called when it
shouldn't have`)
}
最后一个测试用例是确保如果用户的数据不在高速缓存中,则将从数据库中正确获取该数据。 该测试通过刷新Redis(清除其所有数据)并再次调用GetSongsByUser()
来实现此GetSongsByUser()
。 这次,将调用已检测的函数,并且测试将验证callCount
等于1。最后,还原原始的GetSongsByUser_DB()
函数。
// Clear the cache
dl.GetRedis().FlushDB()
// Get the songs again, now it's should go to the DB
// because the cache is empty
_, err = lm.GetSongsByUser(u)
if err != nil {
t.Error("GetSongsByUser() failed")
}
// Verify the DB was accessed because the cache is empty
if callCount != 1 {
t.Error(`GetSongsByUser_DB() wasn't called once
as it should have`)
}
GetSongsByUser_DB = originalFunc
}
缓存无效
我们的缓存非常基础,不会做任何无效操作。 只要所有歌曲都通过用于更新Redis的AddSong()
方法添加,该方法就可以很好地工作。 如果我们添加更多操作(如删除歌曲或删除用户),则这些操作应相应地负责更新Redis。
即使我们有一个分布式系统,只要所有实例都可以在同一个DB和Redis实例上运行,那么即使我们有一个分布式系统,其中多个独立的机器都可以运行我们的Songify服务,这种非常简单的缓存也将起作用。
但是,如果由于维护操作或其他工具和应用程序更改我们的数据而导致数据库和缓存不同步,那么我们需要为缓存提出无效和刷新策略。 可以使用相同的技术对其进行测试-替换目标函数或直接访问测试中的DB和Redis以验证状态。
LRU缓存
通常,您不能只是让缓存无限增长。 将最有用的数据保留在缓存中的常见方案是LRU缓存(最近最少使用)。 最早的数据在达到容量时会从缓存中溢出。
测试它涉及在测试期间将容量设置为相对较小的数量,超过容量,并确保最旧的数据不再在缓存中,并且访问它需要数据库访问。
测试数据完整性
您的系统仅与数据完整性一样好。 如果您有损坏的数据或丢失的数据,那么您的状态就很糟糕。 在实际系统中,很难保持完美的数据完整性。 架构和格式会发生变化,数据可能会通过无法检查所有约束的通道进行提取,错误数据会导致错误,管理员尝试进行手动修复,备份和还原可能不可靠。
鉴于这种严酷的现实,您应该测试系统的数据完整性。 每次更改代码后,测试数据完整性与常规自动测试有所不同。 原因是即使代码未更改,数据也可能变质。 您肯定希望在代码更改后运行数据完整性检查,这可能会更改数据存储或表示,但也要定期运行它们。
测试约束
约束是数据建模的基础。 如果使用关系数据库,则可以在SQL级别定义一些约束,然后让数据库强制实施。 空值,文本字段的长度,唯一性和1-N关系可以轻松定义。 但是SQL无法检查所有约束。
例如,在Desongcious中,用户和歌曲之间存在NN关系。 每首歌曲必须与至少一个用户相关联。 没有在SQL中强制执行此操作的好方法(很好,您可以在歌曲与用户之间使用外键,并且将歌曲指向与之关联的用户之一)。 另一个限制可能是每个用户最多只能拥有500首歌曲。 同样,没有办法用SQL表示它。 如果您使用NoSQL数据存储,那么通常在数据存储级别上对声明和验证约束的支持甚至更少。
这给您提供了两个选择:
- 确保仅通过经过审核的界面和工具强制执行所有约束,才能访问数据。
- 定期扫描数据,查找违反约束的问题并进行修复。
测试幂等
幂等意味着连续执行多次相同的操作将具有与一次执行相同的效果。
例如,将变量x设置为5是幂等的。 您可以将x设置为5一次或一百万次。 仍然是5。但是,将X递增1并不是幂等的。 每个连续的增量都会更改其值。 在具有临时网络分区和恢复协议的分布式系统中,幂等性是非常理想的属性,如果没有即时响应,该协议会重试多次发送消息。
如果将幂等性设计到数据访问代码中,则应该对其进行测试。 这通常很容易。 对于每个幂等操作,您可以扩展为连续执行两次或多次该操作,并验证没有错误并且状态保持不变。
请注意,幂等设计有时可能会掩盖错误。 考虑从数据库中删除一条记录。 这是一个幂等运算。 删除记录后,该记录在系统中不再存在,并且尝试再次删除它不会将其恢复。 这意味着尝试删除不存在的记录是有效的操作。 但这可能掩盖了调用者传递了错误的记录密钥的事实。 如果返回错误消息,则它不是幂等的。
测试数据迁移
数据迁移可能是非常危险的操作。 有时,您对所有数据或数据的关键部分运行脚本,并进行一些严重的手术。 如果出现问题(例如返回原始数据并找出问题所在),则应该为计划B做好准备。
在许多情况下,数据迁移可能是一项缓慢而昂贵的操作,在迁移过程中可能需要两个系统并排。 我参加了数天甚至数周的几次数据迁移。 面对大规模数据迁移时,值得花一些时间并在数据的一小部分(但很有代表性)上测试迁移本身,然后验证新迁移的数据是否有效以及系统是否可以使用它。
测试丢失的数据
数据丢失是一个有趣的问题。 有时丢失的数据会破坏您的数据完整性(例如,丢失其用户的歌曲),有时又丢失了(例如,某人删除了用户及其所有歌曲)。
如果丢失的数据导致数据完整性问题,那么您将在数据完整性测试中检测到它。 但是,如果只是缺少一些数据,则没有简便的方法可以检测到它们。 如果数据从未进入持久性存储,则日志或其他临时存储中可能存在跟踪。
根据丢失数据有多少风险,您可以编写一些测试来故意从系统中删除一些数据,并验证系统是否按预期运行。
结论
测试数据密集型代码需要仔细计划并了解您的质量要求。 您可以在多个抽象级别进行测试,您的选择将影响测试的彻底性和全面性,测试的实际数据层的多个方面,测试的运行速度以及在测试过程中修改测试的难易程度。数据层更改。
没有一个正确的答案。 从超级全面,缓慢而费力的测试到快速,轻量级的测试,您都需要在整个范围内找到自己的最佳位置。
翻译自: https://code.tutsplus.com/tutorials/testing-data-intensive-code-with-go-part-5--cms-29852