Bienvenue, les amis ! Aujourd'hui, nous aborderons les tests de bout en bout dans un article de blog intrigant. Si vous n'avez jamais rédigé ce genre de tests ou si vous vous efforcez de les améliorer, continuez à lire pendant que je vous guide tout au long de ce voyage passionnant. À la fin de l'article, vous saurez comment renforcer l'utilisation du package testcontainers-go pour faire briller votre suite de tests.
Avant d'aller de l'avant, fixons les limites de cet article de blog puisque nous aborderons plusieurs concepts, outils et techniques.
Étant donné que nous aborderons plusieurs sujets dans le reste de l'article de blog, je pense que c'est une bonne idée de les rassembler ici.
Les outils que je présente tout au long de cet article de blog sont un mélange d'outils que je connais bien et d'autres que j'ai utilisés pour la première fois. Essayez de ne pas utiliser ces outils sans réfléchir, mais évaluez-les en fonction de votre scénario.
Nous allons nous appuyer sur :
Pour éviter de gonfler la lecture, je ne couvrirai pas tous les aspects et facettes des sujets présentés ici. Je mettrai les URL de documentation pertinentes si nécessaire.
Supposons que nous devions écrire des tests de bout en bout sur un projet qui ne nous appartient pas. Dans mon cas, je souhaite écrire des tests de bout en bout sur un projet écrit avec le langage de programmation Java. Comme je ne savais pas coder en Java, mon option de test consistait uniquement en des tests de bout en bout. Le service que je devais tester était un ensemble d'API REST. La solution était évidente : exercer les points de terminaison en émettant des requêtes HTTP.
Il permet de tester les fonctionnalités exposées comme une boîte noire. Nous n'avons à nous soucier que de la surface publique : ce que nous envoyons au serveur et ce que nous en récupérons. Rien de plus, rien de moins.
Nous nous soucions du point de terminaison api/accounts qui répertorie les comptes bancaires dans notre base de données (une instance MySQL). Nous allons émettre ces deux demandes :
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.
In this section, I present all the relevant code we need to write for the dreaded end-to-end tests.
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:
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 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:
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.
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.
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.
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:
Having the code written this way can help you avoid the hassle of dealing with docker cli commands and makefiles.
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 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.
これを追加するのを忘れても、Go コンパイラーが警告を発するため、心配する必要はありません。
次に進みましょう。
このノードを使用すると、共通のセットアップ ロジックを抽出できます。このコード部分は、スイート内のテストの1 回と前に実行されます。何が行われているかを要約しましょう:
このブログ投稿をさらに長くしたくないので、テスト コードを簡略化しました?.
テスト関数は Describe コンテナ ノード 内に存在します。 ginkgo がテスト仕様をどのように処理するかについては、ここを参照してください。 Describe ノードを使用すると、スコープに基づいてテストをグループ化および整理できます。このノードを他の Describe ノード内にネストできます。
Describe ノードをネストするほど、テスト範囲が狭くなります。
次に、親 Describe を修飾するコンテキスト コンテナ ノード があります。これにより、テストが有効となる状況が決まります。最後に、「It」セクション、つまり「Spec Subject」があります。これは私たちが実行している実際のテストであり、階層ツリーのリーフ レベルです。テスト コードは一目瞭然なので、テストを実行するセクションに進みます。
おめでとうございます?なんとかここにたどり着くことができました。今は、テスト実行操作だけを見逃しています。瞬く間に、テスト実行レポートが端末に印刷されます。
ターミナルに切り替えて、コマンド ginkgo --tags=integration -v を実行してみましょう。しばらくすると、出力がターミナルに表示されます。
このブログ投稿には多くのことが凝縮されていると思います。私の目標は、優れたテスト スイートを作成する方法についての洞察とアプローチを提供することです。提示されたツール、パッケージ、テクニックを他の種類のテストやユースケースに適応させたい場合があります。
終了する前に、testcontainers-go パッケージの compose モジュールのもう 1 つの美しさを強調しておきたいと思います。
私が提供した構成をそのまま使用すれば、必ず最新の Docker イメージを使用できるため、古いイメージの使用による何時間ものトラブルシューティングを回避できます。これは、コマンド docker compose build --no-cache && docker compose up に似ています。感謝してくれるだろうか?
皆さん、ご清聴ありがとうございました!ご質問、疑問、フィードバック、コメントがございましたら、一緒に聞いたり話したりすることができます。特定の概念について説明してほしい場合は、私までご連絡ください。次回まで、気をつけてお会いしましょう?
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!