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

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

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

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]刪除
最新教學 更多>
  • 為什麼不使用CSS`content'屬性顯示圖像?
    為什麼不使用CSS`content'屬性顯示圖像?
    在Firefox extemers屬性為某些圖像很大,&& && && &&華倍華倍[華氏華倍華氏度]很少見,卻是某些瀏覽屬性很少,尤其是特定於Firefox的某些瀏覽器未能在使用內容屬性引用時未能顯示圖像的情況。這可以在提供的CSS類中看到:。 googlepic { 內容:url(&...
    程式設計 發佈於2025-05-19
  • 如何使用node-mysql在單個查詢中執行多個SQL語句?
    如何使用node-mysql在單個查詢中執行多個SQL語句?
    Multi-Statement Query Support in Node-MySQLIn Node.js, the question arises when executing multiple SQL statements in a single query using the node-mys...
    程式設計 發佈於2025-05-19
  • 用戶本地時間格式及時區偏移顯示指南
    用戶本地時間格式及時區偏移顯示指南
    在用戶的語言環境格式中顯示日期/時間,並使用時間偏移在向最終用戶展示日期和時間時,以其localzone and格式顯示它們至關重要。這確保了不同地理位置的清晰度和無縫用戶體驗。以下是使用JavaScript實現此目的的方法。 方法:推薦方法是處理客戶端的Javascript中的日期/時間格式化和...
    程式設計 發佈於2025-05-19
  • 如何在其容器中為DIV創建平滑的左右CSS動畫?
    如何在其容器中為DIV創建平滑的左右CSS動畫?
    通用CSS動畫,用於左右運動 ,我們將探索創建一個通用的CSS動畫,以向左和右移動DIV,從而到達其容器的邊緣。該動畫可以應用於具有絕對定位的任何div,無論其未知長度如何。 問題:使用左直接導致瞬時消失 更加流暢的解決方案:混合轉換和左 [並實現平穩的,線性的運動,我們介紹了線性的轉換。...
    程式設計 發佈於2025-05-19
  • 為什麼使用Firefox後退按鈕時JavaScript執行停止?
    為什麼使用Firefox後退按鈕時JavaScript執行停止?
    導航歷史記錄問題:JavaScript使用Firefox Back Back 此行為是由瀏覽器緩存JavaScript資源引起的。要解決此問題並確保在後續頁面訪問中執行腳本,Firefox用戶應設置一個空功能。 警報'); }; alert('inline Alert')...
    程式設計 發佈於2025-05-19
  • 為什麼我的CSS背景圖像出現?
    為什麼我的CSS背景圖像出現?
    故障排除:CSS背景圖像未出現 ,您的背景圖像儘管遵循教程說明,但您的背景圖像仍未加載。圖像和样式表位於相同的目錄中,但背景仍然是空白的白色帆布。 而不是不棄用的,您已經使用了CSS樣式: bockent {背景:封閉圖像文件名:背景圖:url(nickcage.jpg); 如果您的html,cs...
    程式設計 發佈於2025-05-19
  • 如何避免Go語言切片時的內存洩漏?
    如何避免Go語言切片時的內存洩漏?
    ,a [j:] ...雖然通常有效,但如果使用指針,可能會導致內存洩漏。這是因為原始的備份陣列保持完整,這意味著新切片外部指針引用的任何對象仍然可能佔據內存。 copy(a [i:] 對於k,n:= len(a)-j i,len(a); k
    程式設計 發佈於2025-05-19
  • 如何簡化PHP中的JSON解析以獲取多維陣列?
    如何簡化PHP中的JSON解析以獲取多維陣列?
    php 試圖在PHP中解析JSON數據的JSON可能具有挑戰性,尤其是在處理多維數組時。要簡化過程,建議將JSON作為數組而不是對象解析。 執行此操作,將JSON_DECODE函數與第二個參數設置為true:[&&&&& && &&&&& json = JSON = JSON_DECODE($ ...
    程式設計 發佈於2025-05-19
  • 如何干淨地刪除匿名JavaScript事件處理程序?
    如何干淨地刪除匿名JavaScript事件處理程序?
    刪除匿名事件偵聽器將匿名事件偵聽器添加到元素中會提供靈活性和簡單性,但是當要刪除它們時,可以構成挑戰,而無需替換元素本身就可以替換一個問題。 element? element.addeventlistener(event,function(){/在這里工作/},false); 要解決此問題,請考...
    程式設計 發佈於2025-05-19
  • eval()vs. ast.literal_eval():對於用戶輸入,哪個Python函數更安全?
    eval()vs. ast.literal_eval():對於用戶輸入,哪個Python函數更安全?
    稱量()和ast.literal_eval()中的Python Security 在使用用戶輸入時,必須優先確保安全性。強大的Python功能Eval()通常是作為潛在解決方案而出現的,但擔心其潛在風險。 This article delves into the differences betwee...
    程式設計 發佈於2025-05-19
  • 如何將PANDAS DataFrame列轉換為DateTime格式並按日期過濾?
    如何將PANDAS DataFrame列轉換為DateTime格式並按日期過濾?
    Transform Pandas DataFrame Column to DateTime FormatScenario:Data within a Pandas DataFrame often exists in various formats, including strings.使用時間數據時...
    程式設計 發佈於2025-05-19
  • 在UTF8 MySQL表中正確將Latin1字符轉換為UTF8的方法
    在UTF8 MySQL表中正確將Latin1字符轉換為UTF8的方法
    在UTF8表中將latin1字符轉換為utf8 ,您遇到了一個問題,其中含義的字符(例如,“jáuòiñe”)在utf8 table tabled tablesset中被extect(例如,“致電。為了解決此問題,您正在嘗試使用“ mb_convert_encoding”和“ iconv”轉換受...
    程式設計 發佈於2025-05-19
  • Java開發者如何保護數據庫憑證免受反編譯?
    Java開發者如何保護數據庫憑證免受反編譯?
    在java 在單獨的配置文件保護數據庫憑證的最有效方法中存儲憑據是將它們存儲在單獨的配置文件中。該文件可以在運行時加載,從而使登錄數據從編譯的二進製文件中遠離。 使用prevereness class import java.util.prefs.preferences; 公共類示例{ 首選...
    程式設計 發佈於2025-05-19
  • 為什麼使用固定定位時,為什麼具有100%網格板柱的網格超越身體?
    為什麼使用固定定位時,為什麼具有100%網格板柱的網格超越身體?
    網格超過身體,用100%grid-template-columns 為什麼在grid-template-colms中具有100%的顯示器,當位置設置為設置的位置時,grid-template-colly修復了? 問題: 考慮以下CSS和html: class =“ snippet-code”> ...
    程式設計 發佈於2025-05-19

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

Copyright© 2022 湘ICP备2022001581号-3