Home >Backend Development >Golang >Leverage Your Test Suite With testcontainers-go & docker-compose

Leverage Your Test Suite With testcontainers-go & docker-compose

Linda Hamilton
Linda HamiltonOriginal
2024-10-04 16:07:02186browse

Leverage Your Test Suite With testcontainers-go & docker-compose

Welcome back, folks! Today, we will cover the end-to-end tests in an intriguing blog post. If you've never written these kinds of tests or if you strive to improve them, keep reading as I'll walk you through this exciting journey. By the end of the article, you'll know how to empower the usage of the testcontainers-go package to let your test suite shine.

The Premise ?

Before moving ahead, let's set the boundaries for this blog post since we will cover several concepts, tools, and techniques.

The Survival List ?️

Since we'll touch on several topics throughout the rest of the blog post, I feel it's a good idea to put them together here.

The tools I present throughout this blog post are a mix of tools I know well and some I used for the first time. Try not to use these tools without thinking, but evaluate them based on your scenario.

We're going to rely on:

  • The Go programming language
  • Docker
  • The testcontainers-go package with the compose module
  • The ginkgo testing framework and the gomega assertion package

To avoid bloating the reading, I won't cover every aspect and facet of the topics presented here. I will put the relevant documentation URLs where needed.

The Scenario ?

Let's assume we need to write end-to-end tests on a project we don't own. In my case, I want to write end-to-end tests on a project written with the Java programming language. Since I didn't know how to code in Java, my testing option was only end-to-end tests. The service I had to test was a set of REST APIs. The solution has been obvious: exercise the endpoints by issuing HTTP requests.

It allows testing the exposed features like a black box. We only have to deal with the public surface: what we send to the server and what we get back from it. Nothing more, nothing less.

We care about the api/accounts endpoint that lists the bank accounts in our database (a MySQL instance). We're going to issue these two requests:

HTTP Method Address Expected Status Code
GET api/accounts?iban=IT10474608000005006107XXXXX 200 StatusOK
GET api/accounts?iban=abc 400 StatusBadRequest

现在,您应该对我们的目标有了更清晰的了解。那么,让我们进入测试代码。

让我们玩得开心吗?

在本节中,我将介绍我们需要为可怕的端到端测试编写的所有相关代码。

docker-compose.yml 文件

由于我们不关心源代码,因此起点是 docker-compose.yml 文件。相关代码为:


services:
  mysqldb:
    image: "mysql:8.0"
    container_name: mysqldb
    restart: always
    ports:
      - 3307:3306
    networks:
      - springapimysql-net
    environment:
      MYSQL_DATABASE: transfers_db
      MYSQL_USER: bulk_user
      MYSQL_PASSWORD: root
      MYSQL_ROOT_PASSWORD: root
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  api_service:
    build: .
    container_name: api_service
    restart: always
    ports:
      - 8080:8080
    networks:
      - springapimysql-net
    environment:
      - spring.datasource.url=jdbc:mysql://mysqldb:3306/transfers_db
      - spring.datasource.username=bulk_user
      - spring.datasource.password=root
    depends_on:
      mysqldb:
        condition: service_healthy
    volumes:
      - .m2:/root/.m2

networks:
  springapimysql-net:



文件内容非常简单。我们可以总结以下列表中定义的内容:

  • mysqldb 服务不需要任何进一步的解释
  • api_service 服务是我们正在测试的系统
  • springapimysql-net 网络托管上面定义的两个服务

有关更多 Docker Compose 参考,您可以查看此处。现在,让我们看看端到端的测试代码。

银杏测试框架

ginkgo 测试框架帮助我们构建测试套件。它完全是用 Go 编写的。此外,它还提供了一个 CLI 实用程序来设置和运行测试。因为稍后我们会用到它,所以我们从这里下载它。您可以通过两种方式下载:

  1. 使用 go install 命令(如果您已在系统上安装了 Go)
  2. 通过下载已编译的二进制文件(如果您的系统上没有安装 Go,则很有用)

要检查您的计算机上是否有可用的实用程序,您可以运行命令 ginkgo version (在撰写本文时,我的版本是 2.20.2)。

请注意,ginkgo 命令不是运行测试所必需的。您仍然可以通过坚持 go test 命令来在没有此实用程序的情况下运行测试。

但是,我强烈建议下载它,因为我们将使用它来生成一些样板代码。

以银杏打基础

位于根目录中,让我们创建一个名为 end2end 的文件夹来托管我们的测试。在该文件夹中,通过发出命令 go mod init path/to/your/module 来初始化 Go 模块

现在,是时候运行 ginkgo bootstrap 命令了。它应该生成一个名为 end2end_suite_test.go 的新文件。该文件会触发我们稍后定义的测试套件。

此方法类似于 testify/suite 包的方法。由于定义和运行阶段是分开的,它增强了代码的模块化和鲁棒性。

现在,让我们将测试添加到我们的套件中。要生成我们的测试所在的文件,请运行另一个 ginkgo 命令:ginkgogeneratecounts。这次,文件accounts_test.go 弹出。现在,让我们保持原样并切换到终端。我们通过运行 Go 命令 go mod tidy 将缺少的依赖项下载到我们的计算机本地来修复丢失的软件包。

end2end_suite_test.go 文件

让我们从测试套件的入口点开始。文件内容看起来很整洁:


//go:build integration

package end2end

import (
 "testing"

 . "github.com/onsi/ginkgo/v2"
 . "github.com/onsi/gomega"
)

func TestEnd2End(t *testing.T) {
 RegisterFailHandler(Fail)
 RunSpecs(t, "End2End Suite")
}



唯一不寻常的事情可能是导入部分中的点导入。您可以在此处的文档中阅读有关它的更多信息。

哎哟!出现野生测试容器?

在某些时候,我们需要一些魔法才能进入下一个测试级别。恰好是 testcontainers-go。为了这个演示,我们使用 compose 模块(更多参考,请参阅此处)。

该工具可以运行我们之前看到的 compose 文件,并对正在运行的容器执行端到端测试。

这是 testcontainers-go 功能的摘录。如果您想了解更多信息,请参阅文档或联系我们。我很乐意带您了解其令人惊叹的功能。

这个包允许使用单个命令运行端到端套件。这是运行这些测试的更加一致和原子的方式。它让我能够:

  1. 在套件之前启动容器
  2. 依赖这些容器运行测试
  3. 套件后拆除容器并清理所使用的资源

以这种方式编写代码可以帮助您避免处理 docker cli 命令和 makefile 的麻烦。

accounts_test.go 文件

现在,让我们看看我们的测试所在的代码。


//go:build integration

package end2end

import (
 "context"
 "net/http"
 "os"

 . "github.com/onsi/ginkgo/v2"
 . "github.com/onsi/gomega"
 tc "github.com/testcontainers/testcontainers-go/modules/compose"
 "github.com/testcontainers/testcontainers-go/wait"
)

var _ = Describe("accounts", Ordered, func() {
 BeforeAll(func() {
 os.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true")
 composeReq, err := tc.NewDockerComposeWith(tc.WithStackFiles("../docker-compose.yml"))
  Expect(err).Should(BeNil())
  DeferCleanup(func() {
   Expect(composeReq.Down(context.Background(), tc.RemoveOrphans(true), tc.RemoveImagesLocal)).Should(BeNil())
  })
 ctx, cancel := context.WithCancel(context.Background())
  DeferCleanup(cancel)
 composeErr := composeReq.
   WaitForService("api_service", wait.ForListeningPort("8080/tcp")).
   Up(ctx, tc.Wait(true))
  Expect(composeErr).Should(BeNil())
 })

 Describe("retrieving accounts", func() {
  Context("HTTP request is valid", func() {
   It("return accounts", func() {
 client := http.Client{}
 r, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8080/api/accounts?iban=IT10474608000005006107XXXXX", nil)
 res, err := client.Do(r)
    Expect(err).Should(BeNil())
    Expect(res).To(HaveHTTPStatus(http.StatusOK))
   })
  })

  Context("HTTP request is NOT valid", func() {
   It("err with invalid IBAN", func() {
 client := http.Client{}
 r, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8080/api/accounts?iban=abcd", nil)
    Expect(err).Should(BeNil())
 res, err := client.Do(r)
    Expect(err).Should(BeNil())
    Expect(res).To(HaveHTTPStatus(http.StatusBadRequest))
   })
  })
 })
})



乍一看,似乎很难消化。为了让事情变得更简单,让我们把它分成更小的部分。

描述容器节点

Describe 容器节点只不过是一个包装器,用于保存我们套件的相关代码。一切都必须生活在其中。它是脚手架代码的一部分:var _ =Describe("accounts", Ordered, func() {}。在 {} 中,您应该放置所有相关代码。要强制使用设置节点(像BeforeAll一样),我们必须将Describe容器定义为Ordered。

如果您忘记添加它,请不要担心,因为 Go 编译器会抱怨。

我们继续吧。

BeforeAll 设置节点

这个节点允许我们提取通用的设置逻辑。此代码部分在套件内的测试之前执行一次。让我们回顾一下正在做的事情:

  • 将环境变量 TESTCONTAINERS_RYUK_DISABLED 设置为 true。您可以在此处了解配置信息。如果你对 Ryuk 感到好奇,你可能想看看这个
  • 根据我们提供的 docker-compose.yml 文件创建一个 *tc.DockerCompose 变量
  • 推迟函数调用以终止容器和清理资源
  • 启动撰写堆栈并等待名为 api_service 的容器启动并准备好侦听 8080/tcp 端口

我简化了测试代码,因为我不想让这篇博文变得更长?.

最后,测试! ?

测试函数位于描述容器节点中。您可以参考这里了解ginkgo是如何处理测试规范的。描述节点允许您根据测试范围对测试进行分组和组织。您可以将此节点嵌套在其他描述节点中。

Describe节点嵌套越多,测试范围就越窄。

然后,我们就有了限定父描述的上下文容器节点。它限定了测试有效的情况。最后,我们有 It 部分,即 Spec 主题。这是我们正在执行的实际测试,并且是层次结构树的叶级别。测试代码是不言自明的,因此我将跳转到运行测试的部分。

3, 2, 1...?

恭喜?我们设法到达这里。现在,我们只错过了试运行操作。眨眼间,我们的测试执行报告就会打印到终端上。

让我们切换到终端并运行命令 ginkgo --tags=integration -v。一段时间后,您将看到终端上打印的输出。

结束语 ?

我知道这篇博文中浓缩了很多内容。我的目标是提供有关如何编写良好的测试套件的见解和方法。您可能希望使所提供的工具、包和技术适应其他类型的测试或用例。

在离开之前,我想强调一下 testcontainers-go 包的 compose 模块的另一个优点。

如果您坚持使用我提供的配置,您一定会使用最新的 Docker 镜像,并且可以避免由于使用过时的镜像而花费数小时进行故障排除。它类似于命令 docker compose build --no-cache && docker compose up。你会感谢我吗?

感谢各位朋友的关注!如果您有任何问题、疑虑、反馈或意见,我可以一起倾听和发言。如果您希望我介绍一些具体概念,请与我联系。下次再见,保重,再见?

The above is the detailed content of Leverage Your Test Suite With testcontainers-go & docker-compose. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn