」工欲善其事,必先利其器。「—孔子《論語.錄靈公》
首頁 > 程式設計 > 透過 testcontainers-go 和 docker-compose 來利用您的測試套件

透過 testcontainers-go 和 docker-compose 來利用您的測試套件

發佈於2024-11-02
瀏覽:303

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

Now, you should have a clearer idea of our goal. So, let's jump into the test code.

Let's Have Fun ?

In this section, I present all the relevant code we need to write for the dreaded end-to-end tests.

The docker-compose.yml file

Since we don't bother with the source code, the starting point is the docker-compose.yml file. The relevant code is:


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:



The file content is pretty straightforward. We can summarize the things defined in the following list:

  • The mysqldb service doesn't deserve any further explanations
  • The api_service service is the system we're testing
  • The springapimysql-net network hosts the two services defined above

For further Docker Compose reference, you may have a look here. Now, let's see the end-to-end test code.

The ginkgo Testing Framework

The ginkgo testing framework helps us in building the test suite. It's entirely written in Go. Furthermore, it provides a CLI utility to set up and run the tests. Since we will use it later, let's download it from here. You can download it in two ways:

  1. By using the go install command (if you've installed Go on your system)
  2. By downloading the compiled binary (useful if you don't have Go installed on your system)

To check whether you have a working utility on your machine, you can run the command ginkgo version (at the time of writing, I have the version 2.20.2).

Please note that the ginkgo command is not mandatory to run the tests. You can still run the tests without this utility by sticking to the go test command.

However, I strongly suggest downloading it since we will use it to generate some boilerplate code.

Lay the Foundation with Ginkgo

Located in the root directory, let's create a folder called end2end to host our tests. Within that folder, initialize a Go module by issuing the command go mod init path/to/your/module.

Now, it's time to run the command ginkgo bootstrap. It should generate a new file called end2end_suite_test.go. This file triggers the test suite we'll define in a bit.

This approach is similar to the one with the testify/suite package. It enforces the code modularity and robustness since the definition and running phases are separated.

Now, let's add the tests to our suite. To generate the file where our tests will live, run another ginkgo command: ginkgo generate accounts. This time, the file accounts_test.go pops out. For now, let's leave it as is and switch to the terminal. We fix the missing packages by running the Go command go mod tidy to download the missing dependencies locally on our machine.

The end2end_suite_test.go file

Let's start with the entry point of the test suite. The content of the file looks neat:


//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")
}



The only unusual thing might be the dot-import within the import section. You can read more about it in the documentation here.

Whoop! A wild testcontainers appears ?

At some points, we need some magic to get to the next testing level. It happened to be testcontainers-go. For the sake of this demo, we use the compose module (for further reference, please refer to here).

This tool can run the compose file we saw earlier and execute the end-to-end tests against the running containers.

This is an extract of the testcontainers-go capabilities. If you want to learn more, please refer to the doc or reach out. I'll be happy to walk you through its stunning features.

This package allows running the end-to-end suite with a single command. It's a more consistent and atomic way to run these tests. It allows me to:

  1. Start the containers before the suite
  2. Run the tests relying on these containers
  3. Teardown of containers after the suite and cleanup of the resources used

Having the code written this way can help you avoid the hassle of dealing with docker cli commands and makefiles.

The accounts_test.go file

Now, let's look at the code where our tests live.


//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))
   })
  })
 })
})



At first glimpse, it might seem hard to digest. To keep things easier, let's break it down into smaller parts.

The Describe container node

The Describe container node is nothing but a wrapper to hold the relevant code for our suite. Everything must live within it. It's part of the scaffolded code: var _ = Describe("accounts", Ordered, func() {}. Within the {}, you should put all of the relevant code. To enforce the usage of setup nodes (like BeforeAll), we must define the Describe container as Ordered.

Do not worry if you forgot to add it since the Go compiler will complain.

Let's move on.

The BeforeAll setup node

This node allows us to extract the common setup logic. This code portion executes once and before the tests within the suite. Let's recap what's doing:

  • Set the environment variable TESTCONTAINERS_RYUK_DISABLED to true. You can learn about the configuration here. If you're curious about Ryuk, you may want to look at this
  • Create a *tc.DockerCompose variable based on the docker-compose.yml file we provided
  • Defer the function invocation to terminate containers and cleanup of the resources
  • Start the compose stack and wait for the container called api_service to be up and ready to listen on the 8080/tcp port

I simplified the test code since I don't want to make this blog post even longer ?.

Finally, the tests! ?

The test functions live within a Describe Container Node. You can find out how ginkgo handles the test specifications by referring to here. The Describe node allows you to group and organize tests based on their scope. You can nest this node inside other Describe ones.

The more you nest the Describe node, the more you narrow the test scope.

Then, we have the Context Container Node that qualifies the parent Describe. It qualifies the circumstances under which the tests are valid. Finally, we have the It section, the Spec Subject. It's the actual test we're performing and is the leaf level of the hierarchy tree. The test code is self-explanatory, so I'll jump to the section where we run the tests.

3, 2, 1... ?

Congrats ? We managed to get here. Now, we only miss the test-running operations. In the blink of an eye, we'll get our test execution report printed onto the terminal.

Let's switch to the terminal and run the command ginkgo --tags=integration -v. After a while, you'll see the output printed on the terminal.

Closing Notes ?

I know there are a lot of things condensed into this blog post. My goal has been to provide insights and approaches on how to write a good testing suite. You may want to adapt the presented tools, packages, and techniques to other kinds of tests or use cases.

Before leaving, I'd like to underline another beauty of the compose module of the testcontainers-go package.

If you stick to the configuration I provided, you're sure to use the latest Docker images, and you can avoid hours of troubleshooting due to outdated image usage. It's analogous to the command docker compose build --no-cache && docker compose up. You'll thank me ?

Thanks for the attention, folks! If you've got any questions, doubts, feedback, or comments, I'm available to listen and speak together. If you want me to cover some specific concepts, please reach me. Until the next time, take care and see you ?

版本聲明 本文轉載於:https://dev.to/ossan/leverage-your-test-suite-with-testcontainers-go-docker-compose-502e?1如有侵犯,請聯絡[email protected]刪除
最新教學 更多>
  • 何時將成功回呼函數與 jQuery Ajax 呼叫分離?
    何時將成功回呼函數與 jQuery Ajax 呼叫分離?
    從jQuery Ajax 呼叫解耦成功回調函數使用jQuery ajax 從伺服器檢索資料時,通常的做法是定義成功.ajax () 區塊中的回呼函數。這將回調處理與 AJAX 呼叫緊密結合在一起,限制了靈活性和可重複使用性。 要在 .ajax() 區塊之外定義成功回調,通常需要宣告一個用於儲存返回資...
    程式設計 發佈於2024-11-03
  • 極簡設計初學者指南
    極簡設計初學者指南
    我一直是乾淨和簡單的倡導者——這是我的思維最清晰的方式。然而,就像生活中的大多數任務一樣,不同的工作有不同的工具,設計也是如此。在這篇文章中,我將分享我發現的極簡設計實踐,這些實踐有助於創建乾淨簡單的網站、模板和圖形——在有限的空間內傳達必要的內容。 簡單可能比複雜更難:你必須努力讓你的思維清晰,...
    程式設計 發佈於2024-11-03
  • 了解 React 應用程式中的渲染和重新渲染:它們如何運作以及如何優化它們
    了解 React 應用程式中的渲染和重新渲染:它們如何運作以及如何優化它們
    当我们在 React 中创建应用程序时,我们经常会遇到术语渲染和重新渲染组件。虽然乍一看这似乎很简单,但当涉及不同的状态管理系统(如 useState、Redux)或当我们插入生命周期钩子(如 useEffect)时,事情会变得有趣。如果您希望您的应用程序快速高效,那么了解这些流程是关键。 ...
    程式設計 發佈於2024-11-03
  • 如何在 Node.js 中將 JSON 檔案讀入伺服器記憶體?
    如何在 Node.js 中將 JSON 檔案讀入伺服器記憶體?
    在Node.js 中將JSON 檔案讀入伺服器記憶體為了增強伺服器端程式碼效能,您可能需要讀取JSON 對象從文件到記憶體以便快速存取。以下是在Node.js 中實現此目的的方法:同步方法:對於同步檔案讀取,請利用fs(檔案系統)中的readFileSync () 方法模組。此方法將檔案內容作為字串...
    程式設計 發佈於2024-11-03
  • 人工智慧可以提供幫助
    人工智慧可以提供幫助
    我剛剛意識到人工智慧對開發人員有很大幫助。它不會很快接管我們的工作,因為它仍然很愚蠢,但是,如果你像我一樣正在學習編程,可以用作一個很好的工具。 我要求 ChatGpt 為我準備 50 個項目來幫助我掌握 JavaScript,它帶來了令人驚嘆的項目,我相信當我完成這些項目時,這些項目將使我成為 ...
    程式設計 發佈於2024-11-03
  • Shadcn UI 套件 - 管理儀表板和網站模板
    Shadcn UI 套件 - 管理儀表板和網站模板
    Shadcn UI 套件是預先設計的多功能儀表板、網站範本和元件的綜合集合。它超越了 Shadcn 的標準產品,為那些不僅僅需要基礎知識的人提供更先進的設計和功能。 獨特的儀表板模板 Shadcn UI Kit 提供了各種精心製作的儀表板模板。目前,有 7 個儀表板模板可用,隨著時...
    程式設計 發佈於2024-11-03
  • 如何使用正規表示式捕獲多行文字區塊?
    如何使用正規表示式捕獲多行文字區塊?
    符合多行文字區塊的正規表示式符合跨多行的文字可能會為正規表示式建構帶來挑戰。考慮以下範例文本:some Varying TEXT DSJFKDAFJKDAFJDSAKFJADSFLKDLAFKDSAF [more of the above, ending with a newline] [yep, ...
    程式設計 發佈於2024-11-03
  • 軟體開發中結構良好的日誌的力量
    軟體開發中結構良好的日誌的力量
    日誌是了解應用程式底層發生的情況的關鍵。 簡單地使用 console.log 列印所有值並不是最有效的日誌記錄方法。日誌的用途不僅僅是顯示數據,它們還可以幫助您診斷問題、追蹤系統行為以及了解與外部 API 或服務的交互作用。在您的應用程式在沒有使用者介面的情況下運行的情況下,例如在系統之間處理和傳...
    程式設計 發佈於2024-11-03
  • 如何在單一命令列命令中執行多行Python語句?
    如何在單一命令列命令中執行多行Python語句?
    在單一命令列指令中執行多行Python語句Python -c 選項允許單行循環執行,但在指令中匯入模組可能會導致語法錯誤。要解決此問題,請考慮以下解決方案:使用Echo 和管道:echo -e "import sys\nfor r in range(10): print 'rob'&quo...
    程式設計 發佈於2024-11-03
  • 尋找數組/列表中的重複元素
    尋找數組/列表中的重複元素
    給定一個整數數組,找到所有重複的元素。 例子: 輸入:[1,2,3,4,3,2,5] 輸出:[2, 3] 暗示: 您可以使用 HashSet 來追蹤您已經看到的元素。如果某個元素已在集合中,則它是重複的。為了保留順序,請使用 LinkedHashSet 來儲存重複項。 使用 HashSet 的 ...
    程式設計 發佈於2024-11-03
  • JavaScript 回呼何時異步?
    JavaScript 回呼何時異步?
    JavaScript 回呼:是否非同步? JavaScript 回呼並非普遍非同步。在某些場景下,例如您提供的 addOne 和 simpleMap 函數的範例,程式碼會同步執行。 瀏覽器中的非同步 JavaScript基於回呼的 AJAX 函數jQuery 中通常是異步的,因為它們涉及 XHR (...
    程式設計 發佈於2024-11-03
  • 以下是根據您提供的文章內容產生的英文問答類標題:

Why does `char` behave differently from integer types in template instantiation when comparing `char`, `signed char`, and `unsigned char`?
    以下是根據您提供的文章內容產生的英文問答類標題: Why does `char` behave differently from integer types in template instantiation when comparing `char`, `signed char`, and `unsigned char`?
    char、signed char 和unsigned char 之間的行為差異下面的程式碼可以成功編譯,但char 的行為與整數類型不同。 cout << getIsTrue< isX<int8>::ikIsX >() << endl; cout ...
    程式設計 發佈於2024-11-03
  • 如何在動態產生的下拉方塊中設定預設選擇?
    如何在動態產生的下拉方塊中設定預設選擇?
    確定下拉框中選定的項目使用 標籤建立下拉清單時,您可以可能會遇到需要將特定選項設定為預設選擇的情況。這在預先填寫表單或允許使用者編輯其設定時特別有用。 在您呈現的場景中, 標籤是使用 PHP 動態產生的,並且您希望根據值儲存在資料庫中。實現此目的的方法如下:設定選定的屬性要在下拉方塊中設定選定的項目...
    程式設計 發佈於2024-11-03
  • Tailwind CSS:自訂配置
    Tailwind CSS:自訂配置
    介紹 Tailwind CSS 是一種流行的開源 CSS 框架,近年來在 Web 開發人員中廣受歡迎。它提供了一種獨特的可自訂方法來創建美觀且現代的用戶介面。 Tailwind CSS 有別於其他 CSS 框架的關鍵功能之一是它的可定製配置。在這篇文章中,我們將討論 Tailwin...
    程式設計 發佈於2024-11-03
  • 使用 jQuery
    使用 jQuery
    什麼是 jQuery? jQuery 是一個快速的 Javascript 函式庫,其功能齊全,旨在簡化 HTML 文件遍歷、操作、事件處理和動畫等任務。 「少寫多做」 MDN 狀態: jQuery使得編寫多行程式碼和tsk變得更加簡潔,甚至一行程式碼.. 使用 jQuery 處理事件 jQuery...
    程式設計 發佈於2024-11-03

免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。

Copyright© 2022 湘ICP备2022001581号-3