用例 – 在BookSwap应用程序中测试并发
本章的最后一节致力于检测 BookSwap Web 应用程序中的并发问题。我们将使用 Go 的竞态检测器以及我们迄今为止学到的测试策略,看看我们可以在 BookSwap 应用程序中发现哪些问题。
您可能想知道为什么我们要担心 BookSwap 应用程序的并发方面,因为在我们迄今为止看到的代码库中我们没有使用任何锁、通道或 goroutine
。Go 的 net/http
库在底层使用 goroutine 来服务 HTTP 请求,因此应用程序仍然可能存在并发问题,即使它没有显式创建自己的 goroutine 和通道。一旦 BookSwap 应用程序从单体应用程序转换为在第8章《测试微服务架构》中讨论的微服务架构中运行,这种影响将进一步放大。
我们已经拥有所有可用的工具来编写可以模拟和验证应用程序并发行为的测试。我们将编写一个测试,该测试并发创建 BookSwap 用户:
func TestUpsertUser_Load(t *testing.T) {
if os.Getenv("LONG") == "" {
t.Skip("Skipping TestUpsertUser_Load in short mode.")
}
userEndpoint := getTestEndpoint(t)
requestBody, err := json.Marshal(map[string]string{
"name": "Concurrent Test User",
"address": "1 London Road",
"post_code": "N1",
"country": "United Kingdom",
})
require.Nil(t, err)
require.NotNil(t, requestBody)
for i := 0; i < LOAD_AMOUNT; i++ {
t.Run("concurrent upsert", func(t *testing.T) {
t.Parallel()
req := bytes.NewBuffer(requestBody)
r, err := http.Post(userEndpoint, "application/json", req)
assert.Nil(t, err)
body, err := io.ReadAll(r.Body)
r.Body.Close()
require.Nil(t, err)
var resp handlers.Response
err = json.Unmarshal(body, &resp)
require.Nil(t, err)
assert.Equal(t, http.StatusOK, r.StatusCode)
assert.Nil(t, err)
assert.NotNil(t, resp)
assert.NotEmpty(t, resp.User.ID)
})
}
}
go
此基准测试并发向 BookSwap 应用程序的 POST /users
端点发送请求:
-
我们声明一个具有通常签名的新测试,它接收一个
*testing.T
参数。此测试仅在将LONG
参数传递给测试运行器时运行,因为它需要 BookSwap 应用程序启动。userEndpoint
由getTestEndpoint
辅助函数根据环境变量返回。为简洁起见,我们在此未包含此函数的实现。 -
在测试的设置中,我们编组一个具有字符串键和值类型的映射,其中包含创建新用户所需的所有 JSON 字段。我们使用标准库中的
json.Marshal
函数来执行此操作。此函数将返回一个字节切片[]byte
,我们将使用它作为 HTTP POST 调用的请求体。 -
我们在
for
循环中重复相同的测试,直到达到LOAD_AMOUNT
常量。测试运行器将根据其可用配置并行运行测试。包含此for
循环很重要;否则,我们的 goroutine 只会进行一次调用。 -
我们使用
t.Parallel
方法设置测试以并行运行。在底层,这会创建多个 goroutine 并将测试迭代分布在它们之间。此方法接收一个函数作为参数,该函数将设置任何本地状态并在测试的每个 goroutine 中运行。 -
在循环中,我们将 JSON 字节切片转换为缓冲区,这是调用
http.Post
函数所必需的。此函数接收usersEndpoint
,其中包含要测试的 URL。 -
一旦 HTTP 请求完成,它将返回一个响应,我们可以对其进行断言。我们确保关闭响应体,以允许另一个 goroutine 重用相同的连接。
测试的简单配置将允许我们使用固定数量的并发请求来测试我们的端点。正如我们在前几章中看到的,此函数根据为应用程序指定的环境变量构造 URL。如果要使用默认值运行,请将 BOOKSWAP_BASE_URL
环境变量设置为 http://localhost
,并将 BOOKSWAP_PORT
环境变量设置为 3000 到您的终端会话。
默认情况下,基准测试将使用的 goroutine 数量等于 GOMAXPROCS
变量。此变量将等于运行它的机器上的 CPU 数量。您的操作系统决定什么算作 CPU,因此对于具有超线程的四核机器,GOMAXPROCS
将为 8。如果您想调整 goroutine 的数量,可以通过更改此环境变量轻松配置。正如我们在第8章《测试微服务架构》中使用 pprof
工具所做的那样,我们启用了竞态检测器运行 BookSwap 应用程序:
$ go run -race cmd/main.go
bash
资源的修改和 goroutine
的修改发生在 BookSwap 应用程序本身中,而不是测试代码中,因此这就是我们检测应用程序而不是测试代码的原因。请记住,BookSwap 应用程序现在依赖于数据库,我们需要运行 PostgreSQL 并将 BOOKSWAP_DB_URL
环境变量设置为 PostgreSQL 连接字符串。
在单独的终端窗口中,我们以通常的方式运行基准测试,因为在测试配置之外并行运行基准测试是不可见的:
$ LONG=true go test chapter09/user_create_test.go -v
bash
然后,我们可以在第一个控制台窗口中发出 Ctrl + C
的 SIGINT
以停止竞态检测器。如果检测到任何数据竞争,它们将与 BookSwap 的日志一起打印到终端。由于竞态检测器与测试代码分开运行,因此我们无法在检测到数据竞争时使测试失败。因此,我们必须监控日志以查看是否检测到数据竞争。
这种简单的技术可用于我们应用程序的端到端测试和集成测试。您可以使用它来实现任何用户旅程或请求序列。然而,我们应该始终记住,竞态检测器是一个有限的工具,并且没有任何数量的测试可以明确和最终证明不存在不可测试的并发问题。