探索 Godog
在本章中,我们对 BookSwap 应用程序进行了许多更改,扩展了其范围和复杂性。现在我们可以轻松地使用 Docker 容器启动和关闭应用程序,是时候将注意力转向编写 E2E 测试了。
在第 5 章《执行集成测试》中,我们讨论了如何编写 BDD 风格的测试。这种测试风格允许我们编写人类可读的测试场景,并使用 Given-When-Then 结构。这些可读的测试不仅可以作为我们项目的文档,还能让我们涉及多个利益相关者,编写真正涵盖应用程序功能的测试。
我们还探讨了 ginkgo 测试库,它允许我们在单元测试中添加 BDD 风格的断言,而 Godog (https://github.com/cucumber/godog) 是另一个用于编写 BDD 风格测试的测试库。ginkgo 主要用于单元测试,而 Godog 提供了额外的代码生成功能,非常适合用于编写集成测试和 E2E 测试。我们将学习如何使用这个出色的库进行集成和 E2E 测试。
以下是 Godog 的一些亮点:
-
与我们之前使用的库不同,Godog 并不使用
go test
命令运行测试,而是使用godog run
命令。此命令不仅生成测试文件,还运行已实现的测试。 -
测试以功能文件的形式组织,这些文件描述了某个功能在特定场景下的预期行为。Godog 使用一种名为 Gherkin 的领域特定语言 ( https://cucumber.io/docs/gherkin/reference/ )。我们将在本章的其余部分探讨如何使用这种格式编写测试。
-
Godog 是一个开源库,由社区和 Cucumber 组织维护。您可以自由浏览源代码,甚至进行贡献。
与我们使用的其他依赖项一样,安装 Godog 可以通过在终端运行 go install
命令来完成:
$ go install github.com/cucumber/godog/cmd/godog@latest
现在我们已经了解了 Godog 的基本用法并成功安装,接下来我们将开始编写我们的第一个功能文件。我们先从一个简单的 BookSwap 应用程序功能文件开始:
Feature: New user signs up
In order to use the BookSwap application
As a new user
I need to be able to sign up.
Background: Verify configuration
Given the BookSwap app is up
Scenario: Sign up
Given user details
When sent to the users endpoint
Then a new user profile is created
这个功能文件描述了 BookSwap 应用程序中关于新用户注册的一部分功能:
-
该功能描述了作为新用户注册应用程序的场景。
-
作为背景步骤,BookSwap 应用程序应该已启动。这使得我们可以在运行整个应用程序时进行 E2E 测试。
-
完成该功能后,将提供以下功能:
-
新用户可以创建用户资料。
-
当他们的资料创建完成后,用户将看到他们的用户摘要并收到用户 ID,允许他们进一步与应用程序进行交互。
-
注册后,用户可以通过其用户 ID 查看个人资料。
-
任何后续与应用程序的交互都不在本功能的范围内。
-
正如我们所讨论的,功能文件基于应用程序的预期用户旅程和请求流程。功能文件应该易于阅读和理解,因此我们应当为其他功能和场景创建单独的文件,并使用非技术性语言。
在下一节中,我们将学习如何实现并运行这个功能文件。
使用 Godog 实现测试
安装 Godog 并概述了我们的第一个功能后,接下来让我们将注意力转向实现这个测试。
实现我们概述的功能测试的主要步骤如下:
-
创建功能文件和测试文件。
-
实现 BookSwap 应用程序功能的测试步骤。
-
运行应用程序和测试。
正如我们之前提到的,我们将使用 Godog 实现 BDD 风格的 E2E 测试,因此在运行测试之前需要确保应用程序已经启动并正常运行。然而,这并不是 Godog 的要求,我们可以使用这个易于使用的库在任何层级编写测试。
创建测试文件
如前所述,Godog 依赖于代码生成来简化开发者的工作。这个过程包括从终端复制代码并自己创建文件。让我们看看涉及的步骤。
步骤 1 – 创建功能文件
功能文件存储在 Go 项目的根目录下的 /features
目录中。由于我们在仓库中使用了项目文件夹,因此我们需要在 /chapter06/features
下创建一个文件。我们将在该目录下创建一个文件并将功能文本添加到其中:
$ mkdir chapter06/features
$ vim chapter06/features/newUserSignsUp.feature
请注意,文件的命名遵循功能名称,这样可以轻松理解该文件关联的功能。
步骤 2 – 生成步骤定义
一旦功能文件包含了我们的文本,就可以使用 Godog 生成所需的步骤。执行 godog run
命令时,终端会打印出以下生成的代码:
func aNewUserProfileIsCreated() error {
return godog.ErrPending
}
func sentToTheUsersEndpoint() error {
return godog.ErrPending
}
func theBookSwapAppIsUp() error {
return godog.ErrPending
}
func userDetails() error {
return godog.ErrPending
}
func InitializeScenario(ctx *godog.ScenarioContext) {
ctx.Step(`^a new user profile is created$`, aNewUserProfileIsCreated)
ctx.Step(`^sent to the users endpoint$`, sentToTheUsersEndpoint)
ctx.Step(`^the BookSwap app is up$`, theBookSwapAppIsUp)
ctx.Step(`^user details$`, userDetails)
}
图 6.4 展示了我们场景中的步骤顺序,以及它们发出的 HTTP 请求:

生成的代码为我们场景中的每个步骤提供了一个函数:
-
aNewUserProfileIsCreated
:此函数向GET /users/{id}
端点发送请求,并验证用户档案是否成功创建。它还将验证是否可以使用分配的用户 ID 成功检索该用户档案。 -
sentToTheUsersEndpoint
:此函数向POST /users
端点发送一个 JSON 负载,并验证该端点是否返回正确的用户详情。它还将获取应用程序为新用户档案生成的用户 ID。 -
theBookSwapAppIsUp
:此函数向GET /
端点发送请求,并验证应用程序是否返回 200 OK 状态码。在生产环境中,我们通常会暴露一个独立的/health
端点,但我们将利用根端点来演示 BookSwap 应用。 -
userDetails
:此函数将创建一个db.User
实例,将其序列化为 JSON 负载,并发送给sentToTheUsersEndpoint
步骤。它还将作为我们测试断言中的期望值(want 变量)。
我们需要实现这些函数,以便调用应用程序的功能。
最终,InitializeScenario
函数将所有这些函数结合成步骤,并按字母顺序排列。当我们实现测试文件时,我们需要根据功能定义正确地排序这些步骤。
虽然生成的代码很简单,但它为我们的测试代码提供了一个框架,并处理了与 Godog 测试运行器的交互。
步骤 3 – 创建测试文件
与常规的单元测试一样,Godog 测试也位于 *_test.go
文件中,并与它们测试的包一起存在。由于我们将测试整个应用程序,因此我们将在根目录下创建一个测试文件,位于 /features
目录的同级。我们创建一个与功能名称相匹配的测试文件,并将生成的代码粘贴到其中:
$ vi /cmd/newUserAddsBook_test.go
虽然测试文件的名称不必完全匹配,但使用匹配的名称会让 Godog 能够将测试与功能进行匹配。
创建好测试文件和代码后,我们再次执行 godog run
。测试运行器将把场景标记为待处理(pending):
Background: Verify configuration
Given the BookSwap app is up # newUserSignsUp_test.go:148 -> theBookSwapAppIsUp
TODO: write pending definition
Scenario: Sign up # features/newUserSignsUp.feature:9
Given user details # newUserSignsUp_test.go:152 -> userDetails
When sent to the users endpoint # newUserSignsUp_test.go:144 -> sentToTheUsersEndpoint
Then a new user profile is created # newUserSignsUp_test.go:140 -> aNewUserProfileIsCreated
方便的是,输出中还打印了每个步骤的行号,显示了我们缺少的实现部分,这样我们就可以轻松找到需要实现的部分。
实现测试步骤
现在,Godog 已经为我们方便地生成了测试步骤的框架,我们开始根据 BookSwap 应用的功能编写测试代码。然而,正如在上一节中所述,我们需要在测试步骤之间传递信息。
在 Godog 中,传递信息的方式是通过链式上下文(chained contexts)。Godog 会在测试步骤之间传递上下文,允许我们以安全的方式在步骤之间传递信息。为了实现这一点,我们需要更改测试步骤的签名,使其接受一个上下文并返回一个上下文和一个错误:
func theBookSwapAppIsUp(ctx context.Context) (context.Context, error) {
// test step implementation
}
测试步骤接受一个上下文并返回一个上下文和错误。Godog 会在底层正确处理这些返回值:将返回的上下文链式传递给后续的测试步骤,并在遇到非 nil
错误时使测试失败。
上下文复习
|
对于我们的目的,我们将使用上下文来携带请求范围的变量。我们将创建一个新的 contextKey
自定义类型,它将携带我们在测试步骤之间传递所需的所有变量:
// contextKey 用于在测试步骤之间传递信息。
type contextKey struct {
UsersURL string
User db.User
}
在我们的例子中,我们将传播 BookSwap 应用的 UsersURL
和已创建用户的期望值。在我们的背景步骤 theBookSwapAppIsUp
中,展示了如何使用上下文将信息传递给后续步骤:
func theBookSwapAppIsUp(ctx context.Context) (context.Context, error) {
url, err := getTestURL()
if err != nil {
return ctx, fmt.Errorf("incorrect config:%v", err)
}
resp, err := http.Get(url)
if err != nil || resp.StatusCode != http.StatusOK {
return ctx, fmt.Errorf("bookswap not up:%v", err)
}
return context.WithValue(ctx, contextKey{}, contextKey{
UsersURL: url + "/users",
}), nil
}
这段代码演示了一个与 REST 端点交互的步骤的实现:
-
我们通过调用
getTestURL
辅助函数来设置我们将要测试的环境的 URL。这个函数根据应用程序指定的环境变量构造 URL。这样,我们就能方便地配置我们的测试,以便在不同的测试环境(本地或远程)中运行。如果您希望使用默认值,可以将BOOKSWAP_BASE_URL
环境变量设置为http://localhost
,并将BOOKSWAP_PORT
环境变量设置为 3000。 -
我们使用
http.Get
方法与定义的 URL 进行交互,并保存错误和响应。我们在之前的章节中已经熟悉了net/http
库,测试中的使用与之前没有区别。 -
如果出现错误或状态码不是 200 OK,我们返回一个错误。这将使该步骤失败并结束测试。
-
最后,在成功的情况下,我们使用
context.WithValue
函数从ctx
参数值创建一个子上下文,并传递一个填充了UsersURL
的contextKey
。在后续步骤中,我们可以使用这个 URL 来进行请求。
另一项我们需要对生成的测试步骤进行的更改是重新排序步骤,以确保它们按照正确的顺序执行。这一步骤在没有使用 Godog 的情况下可能不太直观,但如果忘记了这一点,你的测试将失败,方便追踪错误位置。
运行测试套件
现在,一切实现完成后,是时候开始测试了。首先,我们需要记得启动 BookSwap 应用程序,可以使用以下命令运行:
docker compose -f docker-compose.book-swap.chapter06.yml up --build
除非你已经更改了配置,否则这将把应用程序暴露在 http://localhost:3000
URL 上。你可以通过执行一个 curl
命令来轻松验证应用程序是否在运行:
$ curl --location --request GET 'http://localhost:3000'
{"message":"Welcome to the BookSwap service!"}
如果你看到欢迎响应,则说明应用程序已启动并正确连接到数据库。
应用程序启动后,我们通过以下命令执行测试:
$ cd chapter06 && godog run
终端输出如下所示:
Feature: New user signs up
In order to use the BookSwap application
As a new user
I need to be able to sign up.
Background: Verify configuration
Given the BookSwap app is up # newUserSignsUp_test.go:23 -> theBookSwapAppIsUp
Scenario: Sign up
Given user details # newUserSignsUp_test.go:35 -> userDetails
When sent to the users endpoint # newUserSignsUp_test.go:50 -> sentToTheUsersEndpoint
Then a new user profile is created # newUserSignsUp_test.go:84 -> aNewUserProfileIsCreated
1 scenarios (1 passed)
4 steps (4 passed)
11.876996ms
从终端输出中可以看到,Godog 运行了一个场景,且四个步骤全部通过。你也可以使用 go test
命令运行测试,如果你不想安装 Godog CLI,但这不会格式化测试结果,如前面的输出所示。
我们成功地为扩展了持久化存储的 BookSwap 应用编写并运行了第一个 E2E 测试。这个测试是使用 Godog 开源测试库编写的,它允许我们编写易于阅读的 BDD 风格测试。我们离成为 Go 测试专家已经不远了。