Testcontainers를 사용하면 테스트 시나리오 작업 프로세스가 근본적으로 향상되었습니다. 이 도구 덕분에 통합 테스트를 위한 환경 생성이 더 간단해졌습니다( Kafka를 사용한 테스트에서 격리 문서 참조). 이제 다양한 버전의 데이터베이스, 메시지 브로커 및 기타 서비스가 포함된 컨테이너를 쉽게 시작할 수 있습니다. 통합 테스트의 경우 Testcontainers가 필수적인 것으로 입증되었습니다.
부하 테스트는 기능 테스트보다 덜 일반적이지만 훨씬 더 재미있을 수 있습니다. 그래프를 연구하고 특정 서비스의 성능을 분석하면 진정한 즐거움을 얻을 수 있습니다. 그러한 작업은 드물지만 나에게는 특히 흥미진진합니다.
이 문서의 목적은 일반 통합 테스트와 동일한 방식(Gradle 프로젝트 환경에서 Testcontainer를 사용하는 Spock 테스트 형식)으로 부하 테스트를 위한 설정을 만드는 접근 방식을 보여주는 것입니다. Gatling, WRK, Yandex.Tank와 같은 부하 테스트 유틸리티가 사용됩니다.
도구 세트: Gradle + Spock Framework + 테스트 컨테이너. 구현 변형은 별도의 Gradle 모듈입니다. 사용되는 부하 테스트 유틸리티는 Gatling, WRK 및 Yandex.Tank입니다.
테스트 개체를 사용하는 방법에는 두 가지가 있습니다.
첫 번째 경우에는 프로젝트 버전 및 변경 사항과 무관한 일련의 부하 테스트가 있습니다. 이 접근 방식은 나중에 유지 관리하기가 더 쉽지만 게시된 이미지만 테스트하는 것으로 제한됩니다. 물론 로컬에서 이러한 이미지를 수동으로 구축할 수도 있지만 자동화 수준이 낮고 재현성이 떨어집니다. 필요한 이미지 없이 CI/CD에서 실행하면 테스트가 실패합니다.
두 번째 경우에는 최신 버전의 서비스에서 테스트가 실행됩니다. 이를 통해 로드 테스트를 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 서비스와 데이터베이스라는 두 가지 종속성이 있음을 알 수 있습니다. 이에 대한 설명은 아래에 제공되지만 체계의 모든 구성 요소가 Docker 환경에서 통신하도록 활성화하는 것부터 시작하겠습니다. 네트워크에 대해 설명하겠습니다.
def network = Network.newNetwork()
각 구성 요소에 대한 네트워크 별칭을 제공합니다. 이는 매우 편리하며 통합 매개변수를 정적으로 설명할 수 있습니다.
WireMock과 같은 종속성 및 부하 테스트 유틸리티 자체가 작동하려면 구성이 필요합니다. 이는 컨테이너에 전달할 수 있는 매개변수이거나 컨테이너에 마운트해야 하는 전체 파일 및 디렉터리일 수 있습니다.
또한 컨테이너에서 작업 결과를 검색해야 합니다. 이러한 작업을 해결하려면 두 가지 디렉터리 세트를 제공해야 합니다.
workingDirectory
— 모듈의 리소스 디렉터리( load-tests/
에 직접 있음)
reportDirectory
— 측정항목 및 로그를 포함한 작업 결과에 대한 디렉터리입니다. 이에 대한 자세한 내용은 보고서 섹션에서 설명합니다.샌드박스 서비스는 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
명령어는 로컬 파일 경로와 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
클래스가 사용됩니다. 이는 부하 테스트 시나리오에서는 전체 로그가 필요하지 않을 가능성이 높으며 서비스가 올바르게 작동하는지 평가하는 데 부분 로그이면 충분하기 때문에 수행됩니다.
부하 테스트를 위한 도구는 다양합니다. 이 기사에서는 Gatling, Wrk 및 Yandex.Tank의 세 가지 사용을 고려할 것을 제안합니다. 세 가지 도구는 모두 서로 독립적으로 사용할 수 있습니다.
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을 하드코딩할 수 있습니다.
-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
라는 두 개의 파일로 표시됩니다. 컨테이너 설명의 일부로 구성 파일이 작업 디렉터리로 마운트될 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를 사용하여 부하 테스트 환경을 만드는 방법을 제시했습니다.
테스트 컨테이너는 통합 테스트를 위한 환경 생성을 크게 단순화하여 유연성과 격리를 제공합니다. 부하 테스트의 경우 이 도구를 사용하면 다양한 버전의 서비스 및 데이터베이스가 포함된 필수 컨테이너를 배포할 수 있으므로 테스트를 더 쉽게 수행하고 결과 재현성을 향상시킬 수 있습니다.
Gatling, Wrk 및 Yandex.Tank에 대해 제공된 구성 예제는 컨테이너 설정과 함께 다양한 도구를 효과적으로 통합하고 테스트 매개변수를 관리하는 방법을 보여줍니다.
또한, 애플리케이션 성능 분석 및 개선에 필수적인 테스트 결과를 로깅하고 저장하는 과정을 설명했습니다. 이 접근 방식은 향후 더 복잡한 시나리오와 다른 모니터링 및 분석 도구와의 통합을 지원하도록 확장될 수 있습니다.
이 기사에 관심을 가져주셔서 감사합니다. 유용한 테스트를 작성하는 데 행운이 있기를 바랍니다!