Testcontainers を使用すると、テスト シナリオの操作プロセスが大幅に改善されました。このツールのおかげで、統合テスト用の環境の作成が簡単になりました (記事「 Kafka を使用したテストの分離」を参照)。今では、さまざまなバージョンのデータベース、メッセージ ブローカー、その他のサービスを含むコンテナーを簡単に起動できます。統合テストでは、Testcontainers が不可欠であることが証明されています。
負荷テストは機能テストほど一般的ではありませんが、はるかに楽しいものです。グラフを調べて特定のサービスのパフォーマンスを分析すると、本当に楽しいです。このようなタスクはまれですが、私にとっては特に刺激的です。
この記事の目的は、通常の統合テストが記述されるのと同じ方法、つまり Gradle プロジェクト環境で Testcontainers を使用する Spock テストの形式で、負荷テストのセットアップを作成するアプローチを示すことです。Gatling、WRK、Yandex.Tank などの負荷テスト ユーティリティが使用されます。
ツールセット: Gradle + Spock Framework + Testcontainers。実装バリアントは別の Gradle モジュールです。使用される負荷テスト ユーティリティは、Gatling、WRK、および Yandex.Tank です。
テスト オブジェクトを操作するには、次の 2 つの方法があります。
最初のケースでは、プロジェクトのバージョンや変更に依存しない一連の負荷テストがあります。このアプローチは将来的に保守が容易ですが、公開されたイメージのみのテストに限定されます。もちろん、これらのイメージをローカルで手動でビルドすることもできますが、自動化が不十分で再現性が低下します。必要なイメージがない状態で CI/CD で実行すると、テストは失敗します。
2 番目のケースでは、テストはサービスの最新バージョンで実行されます。これにより、負荷テストを CI に統合し、サービス バージョン間のパフォーマンス データの変更を取得できます。ただし、負荷テストは通常、単体テストよりも時間がかかります。このようなテストを品質ゲートの一部として CI に含めるかどうかは、ユーザーが決定します。
この記事では、最初のアプローチを検討します。Spock のおかげで、比較分析のためにサービスの複数のバージョンでテストを実行できます。
where: image | _ 'avvero/sandbox:1.0.0' | _ 'avvero/sandbox:1.1.0' | _
この記事の目的は、本格的な負荷テストではなく、テストスペースの構成を示すことであることに注意してください。
テスト オブジェクトとして、エンドポイントを公開し、外部ソースからのデータを使用してリクエストを処理する Sandbox という単純な HTTP サービスを使用します。このサービスにはデータベースがあります。
Dockerfile を含むサービスのソース コードは、プロジェクト リポジトリspring-sandboxで入手できます。
この記事の後半で詳細を掘り下げていくので、まずはload-tests
Gradle モジュールの構造を簡単に概説して、その構成を理解していただきたいと思います。
load-tests/ |-- src/ | |-- gatling/ | | |-- scala/ | | | |-- MainSimulation.scala # Main Gatling simulation file | | |-- resources/ | | | |-- gatling.conf # Gatling configuration file | | | |-- logback-test.xml # Logback configuration for testing | |-- test/ | | |-- groovy/ | | | |-- pw.avvero.spring.sandbox/ | | | | |-- GatlingTests.groovy # Gatling load test file | | | | |-- WrkTests.groovy # Wrk load test file | | | | |-- YandexTankTests.groovy # Yandex.Tank load test file | | |-- java/ | | | |-- pw.avvero.spring.sandbox/ | | | | |-- FileHeadLogConsumer.java # Helper class for logging to a file | | |-- resources/ | | | |-- wiremock/ | | | | |-- mappings/ # WireMock setup for mocking external services | | | | | |-- health.json | | | | | |-- forecast.json | | | |-- yandex-tank/ # Yandex.Tank load testing configuration | | | | |-- ammo.txt | | | | |-- load.yaml | | | | |-- make_ammo.py | | | |-- wrk/ # LuaJIT scripts for Wrk | | | | |-- scripts/ | | | | | |-- getForecast.lua |-- build.gradle
プロジェクト リポジトリ - https://github.com/avvero/testing-bench 。
上記の説明から、サービスには https://external-weather-api.com サービスとデータベースの 2 つの依存関係があることがわかります。それらの説明は後述しますが、まずはスキームのすべてのコンポーネントが Docker 環境で通信できるようにすることから始めましょう。ネットワークについて説明します。
def network = Network.newNetwork()
各コンポーネントにネットワーク エイリアスを提供します。これは非常に便利で、統合パラメータを静的に記述できます。
WireMock や負荷テスト ユーティリティなどの依存関係自体が機能するには、構成が必要です。これらは、コンテナーに渡すことができるパラメーター、またはコンテナーにマウントする必要があるファイルとディレクトリ全体である可能性があります。
さらに、コンテナから作業の結果を取得する必要があります。これらのタスクを解決するには、次の 2 セットのディレクトリを提供する必要があります。
workingDirectory
— モジュールのリソース ディレクトリ ( load-tests/
の直下)。
reportDirectory
— メトリックやログを含む作業結果のディレクトリ。詳細については、レポートのセクションで説明します。Sandbox サービスはデータベースとして Postgres を使用します。この依存関係を次のように説明します。
def postgres = new PostgreSQLContainer<>("postgres:15-alpine") .withNetwork(network) .withNetworkAliases("postgres") .withUsername("sandbox") .withPassword("sandbox") .withDatabaseName("sandbox")
この宣言では、サンドボックス サービスがデータベースに接続するために使用するネットワーク エイリアスpostgres
を指定します。データベースとの統合の説明を完了するには、サービスに次のパラメータを指定する必要があります。
'spring.datasource.url' : 'jdbc:postgresql://postgres:5432/sandbox', 'spring.datasource.username' : 'sandbox', 'spring.datasource.password' : 'sandbox', 'spring.jpa.properties.hibernate.default_schema': 'sandbox'
データベース構造は Flyway を使用してアプリケーション自体によって管理されるため、テストでは追加のデータベース操作は必要ありません。
実際のコンポーネントをコンテナ内で実行する可能性、必要性、または要望がない場合は、その API のモックを提供できます。https://external-weather-api.com のサービスでは、WireMock が使用されます。
WireMock コンテナの宣言は次のようになります。
def wiremock = new GenericContainer<>("wiremock/wiremock:3.5.4") .withNetwork(network) .withNetworkAliases("wiremock") .withFileSystemBind("${workingDirectory}/src/test/resources/wiremock/mappings", "/home/wiremock/mappings", READ_WRITE) .withCommand("--no-request-journal") .waitingFor(new LogMessageWaitStrategy().withRegEx(".*https://wiremock.io/cloud.*")) wiremock.start()
WireMock にwithFileSystemBind
モック構成が必要です。withFileSystemBind 命令は、ローカル ファイル パスと Docker コンテナー内のパス間のファイル システム バインディングを記述します。この場合、ローカル マシン上のディレクトリ"${workingDirectory}/src/test/resources/wiremock/mappings"
WireMock コンテナー内の/home/wiremock/mappings
にマウントされます。
以下は、ディレクトリ内のファイル構成を理解するためのプロジェクト構造の追加部分です。
load-tests/ |-- src/ | |-- test/ | | |-- resources/ | | | |-- wiremock/ | | | | |-- mappings/ | | | | | |-- health.json | | | | | |-- forecast.json
モック構成ファイルが正しく読み込まれ、WireMock によって受け入れられるようにするには、ヘルパー コンテナを使用できます。
helper.execInContainer("wget", "-O", "-", "http://wiremock:8080/health").getStdout() == "Ok"
ヘルパー コンテナは次のように記述されます。
def helper = new GenericContainer<>("alpine:3.17") .withNetwork(network) .withCommand("top")
ちなみに、IntelliJ IDEA バージョン 2024.1では WireMock のサポートが導入され、IDE はモック構成ファイルを作成するときに提案を提供します。
サンドボックス サービス コンテナーの宣言は次のようになります。
def javaOpts = ' -Xloggc:/tmp/gc/gc.log -XX:+PrintGCDetails' + ' -XX:+UnlockDiagnosticVMOptions' + ' -XX:+FlightRecorder' + ' -XX:StartFlightRecording:settings=default,dumponexit=true,disk=true,duration=60s,filename=/tmp/jfr/flight.jfr' def sandbox = new GenericContainer<>(image) .withNetwork(network) .withNetworkAliases("sandbox") .withFileSystemBind("${reportDirectory}/logs", "/tmp/gc", READ_WRITE) .withFileSystemBind("${reportDirectory}/jfr", "/tmp/jfr", READ_WRITE) .withEnv([ 'JAVA_OPTS' : javaOpts, 'app.weather.url' : 'http://wiremock:8080', 'spring.datasource.url' : 'jdbc:postgresql://postgres:5432/sandbox', 'spring.datasource.username' : 'sandbox', 'spring.datasource.password' : 'sandbox', 'spring.jpa.properties.hibernate.default_schema': 'sandbox' ]) .waitingFor(new LogMessageWaitStrategy().withRegEx(".*Started SandboxApplication.*")) .withStartupTimeout(Duration.ofSeconds(10)) sandbox.start()
注目すべきパラメータと JVM 設定は次のとおりです。
さらに、サービスの診断結果を保存するためのディレクトリが構成されます。
テスト シナリオの作成と構成の段階で必要になる可能性が高い、ファイルにコンテナのログを表示する必要がある場合は、コンテナを記述するときに次の手順を使用できます。
.withLogConsumer(new FileHeadLogConsumer("${reportDirectory}/logs/${alias}.log"))
この場合、限られた量のログをファイルに書き込むことができるFileHeadLogConsumer
クラスが使用されます。これは、負荷テストのシナリオではログ全体は必要ない可能性があり、サービスが正しく機能しているかどうかを評価するには部分的なログで十分であるためです。
負荷テスト用のツールは多数あります。この記事では、そのうちの 3 つ、Gatling、Wrk、Yandex.Tank の使用を検討することをお勧めします。これら 3 つのツールは、それぞれ独立して使用できます。
Gatling は、Scala で書かれたオープンソースの負荷テスト ツールです。複雑なテスト シナリオの作成が可能で、詳細なレポートを提供します。Gatling のメイン シミュレーション ファイルは、Scala リソースとしてモジュールに接続されているため、構文の強調表示やドキュメント参照のためのメソッドによるナビゲーションなど、IntelliJ IDEA のフル サポートを使用して作業するのに便利です。
Gatling のコンテナ構成は次のとおりです。
def gatling = new GenericContainer<>("denvazh/gatling:3.2.1") .withNetwork(network) .withFileSystemBind("${reportDirectory}/gatling-results", "/opt/gatling/results", READ_WRITE) .withFileSystemBind("${workingDirectory}/src/gatling/scala", "/opt/gatling/user-files/simulations", READ_WRITE) .withFileSystemBind("${workingDirectory}/src/gatling/resources", "/opt/gatling/conf", READ_WRITE) .withEnv("SERVICE_URL", "http://sandbox:8080") .withCommand("-s", "MainSimulation") .waitingFor(new LogMessageWaitStrategy() .withRegEx(".*Please open the following file: /opt/gatling/results.*") .withStartupTimeout(Duration.ofSeconds(60L * 2)) ); gatling.start()
セットアップは他のコンテナとほぼ同じです。
reportDirectory
からレポートのディレクトリをマウントします。workingDirectory
から構成ファイルのディレクトリをマウントします。workingDirectory
からシミュレーション ファイルのディレクトリをマウントします。
さらに、次のパラメータがコンテナに渡されます。
SERVICE_URL
環境変数には、サンドボックス サービスの URL 値が含まれます。ただし、前述のように、ネットワーク エイリアスを使用すると、シナリオ コードに URL を直接ハードコーディングできます。
-s MainSimulation
コマンド。
何がどこに渡されるかを理解するために、プロジェクトのソース ファイル構造を次に示します。
load-tests/ |-- src/ | |-- gatling/ | | |-- scala/ | | | |-- MainSimulation.scala # Main Gatling simulation file | | |-- resources/ | | | |-- gatling.conf # Gatling configuration file | | | |-- logback-test.xml # Logback configuration for testing
これは最後のコンテナであり、完了時に結果が得られると予想されるため、期待値.withRegEx(".*Please open the following file: /opt/gatling/results.*")
を設定します。このメッセージがコンテナ ログに表示されたとき、または60 * 2
秒後にテストは終了します。
このツールのシナリオの DSL については詳しく説明しません。使用されているシナリオのコードはプロジェクト リポジトリで確認できます。
Wrkはシンプルで高速な負荷テスト ツールです。最小限のリソースで大きな負荷を生成できます。主な機能は次のとおりです。
Wrk のコンテナ構成は次のとおりです。
def wrk = new GenericContainer<>("ruslanys/wrk") .withNetwork(network) .withFileSystemBind("${workingDirectory}/src/test/resources/wrk/scripts", "/tmp/scripts", READ_WRITE) .withCommand("-t10", "-c10", "-d60s", "--latency", "-s", "/tmp/scripts/getForecast.lua", "http://sandbox:8080/weather/getForecast") .waitingFor(new LogMessageWaitStrategy() .withRegEx(".*Transfer/sec.*") .withStartupTimeout(Duration.ofSeconds(60L * 2)) ) wrk.start()
Wrk を Sandbox サービスへのリクエストで動作させるには、Lua スクリプトによるリクエスト記述が必要なので、 workingDirectory
からスクリプト ディレクトリをマウントします。 コマンドを使用して、スクリプトとターゲット サービス メソッドの URL を指定して Wrk を実行します。 Wrk は結果に基づいてログにレポートを書き込みます。これを使用して期待値を設定できます。
Yandex.Tank は、Yandex が開発した負荷テスト ツールです。JMeter や Phantom など、さまざまな負荷テスト エンジンをサポートしています。負荷テストの結果を保存および表示するには、無料のサービスOverloadを使用できます。
コンテナの構成は次のとおりです。
copyFiles("${workingDirectory}/src/test/resources/yandex-tank", "${reportDirectory}/yandex-tank") def tank = new GenericContainer<>("yandex/yandex-tank") .withNetwork(network) .withFileSystemBind("${reportDirectory}/yandex-tank", "/var/loadtest", READ_WRITE) .waitingFor(new LogMessageWaitStrategy() .withRegEx(".*Phantom done its work.*") .withStartupTimeout(Duration.ofSeconds(60L * 2)) ) tank.start()
Sandbox の負荷テスト構成は、 load.yaml
とammo.txt
の 2 つのファイルで表されます。コンテナ記述の一部として、構成ファイルはreportDirectory
にコピーされ、作業ディレクトリとしてマウントされます。渡される内容と場所を理解するために、プロジェクト ソース ファイルの構造を次に示します。
load-tests/ |-- src/ | |-- test/ | | |-- resources/ | | | |-- yandex-tank/ | | | | |-- ammo.txt | | | | |-- load.yaml | | | | |-- make_ammo.py
JVM パフォーマンス記録とログを含むテスト結果は、ディレクトリbuild/${timestamp}
に保存されます。ここで、 ${timestamp}
各テスト実行のタイムスタンプを表します。
以下のレポートを確認できます。
ガトリングが使用された場合:
Wrk が使用された場合:
Yandex.Tank が使用された場合:
レポートのディレクトリ構造は次のとおりです。
load-tests/ |-- build/ | |-- ${timestamp}/ | | |-- gatling-results/ | | |-- jfr/ | | |-- yandex-tank/ | | |-- logs/ | | | |-- sandbox.log | | | |-- gatling.log | | | |-- gc.log | | | |-- wiremock.log | | | |-- wrk.log | | | |-- yandex-tank.log | |-- ${timestamp}/ | |-- ...
負荷テストは、ソフトウェア開発ライフサイクルの重要なフェーズです。さまざまな負荷条件下でのアプリケーションのパフォーマンスと安定性を評価するのに役立ちます。この記事では、テスト環境を簡単かつ効率的にセットアップできる Testcontainers を使用して負荷テスト環境を作成するアプローチを紹介しました。
Testcontainers は、統合テストの環境作成を大幅に簡素化し、柔軟性と分離性を実現します。負荷テストの場合、このツールを使用すると、さまざまなバージョンのサービスとデータベースを含む必要なコンテナーを展開できるため、テストの実施が容易になり、結果の再現性が向上します。
提供されている Gatling、Wrk、Yandex.Tank の構成例とコンテナのセットアップにより、さまざまなツールを効果的に統合し、テスト パラメータを管理する方法がわかります。
さらに、アプリケーション パフォーマンスの分析と改善に不可欠な、テスト結果のログ記録と保存のプロセスについても説明しました。このアプローチは、将来的に拡張して、より複雑なシナリオや他の監視および分析ツールとの統合をサポートすることができます。
この記事にご注目いただきありがとうございます。役に立つテストを書くための努力が実りますようお祈り申し上げます。