gatling-best-practices について
gatling-best-practicesは、Java、Kotlin、Scala、JavaScript、TypeScriptでGatlingシミュレーションを作成・レビューするスキルです。5ブロック構成を適用し、設定や負荷テストで起きやすいミスを確認します。
主な機能
- 015つのGatling言語に対応
- 025ブロックのシミュレーション構成
- 03テスト目標に応じた注入プロファイル選択
- 04よくあるシミュレーションの誤りを確認
- 05実行コマンドと検証方法の提示
活用シーン
- 01負荷テスト用シミュレーションの作成
- 02既存Gatlingプロジェクトのレビュー
- 03テスト目標に合う注入プロファイルの選択
- 04実行前の設定問題の発見
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rcampos09/ performance-testing-skills --skill gatling-best-practicesインストール後、Claude Code で/gatling-best-practicesを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
evals/evals.jsonreferences/DESIGN-PATTERNS.mdPROTOCOLS.mdscripts/scaffold.ps1scaffold.shvalidate.sh詳細
Gatling シナリオビルダー
Java、Kotlin、Scala、JavaScript、TypeScriptの、公式にサポートされている5つの言語すべてで、一貫性があり、本番環境に対応した Gatling シミュレーションのパターンを徹底します。
出力形式
シミュレーションを作成または修正するときは、必ず次の3つを提示してください。
- 選択した言語で完全に実行可能なシミュレーションファイル。部分的なスニペットではなく、ユーザーがコピーしてすぐに実行できるものにしてください。
- 環境パラメーター(
baseUrl、usersなど)を含む正確な実行コマンド。 - 選択したインジェクションプロファイルと、指定された負荷目標に適している理由を説明する1行の説明。
ステップ1 — コンテキストを確認する
不明な点だけを質問してください。よくある質問は次のとおりです。
- 言語: Java(最も一般的)・Kotlin・Scala・JavaScript・TypeScript
- ビルドツール: Maven・Gradle・npm(JS/TS のみ)
- プロトコル: HTTP/REST・WebSocket・MQTT・JMS
- 目標: 新規プロジェクトを一から作成・既存のシミュレーションを修正・特定の DSL に関する質問
ユーザーが WebSocket、MQTT、JMS に言及した場合に限り、references/PROTOCOLS.md を読み込んでください。これには、これらのプロトコルの依存関係宣言と DSL が含まれています。見落とされやすい、分かりにくいセットアップ手順です。
ユーザーがフォルダー構成、プロジェクトアーキテクチャ、関心の分離、または単一のシミュレーションファイルを超えて拡張する方法について尋ねた場合に限り、references/DESIGN-PATTERNS.md を読み込んでください。Java、Scala、TypeScript の例を含む、モジュール化された階層パターン(Config → Requests → Scenarios → Simulations)が記載されています。
ステップ2 — 新規プロジェクトの場合は、スキャフォールドスクリプトを使用する
スキャフォールドスクリプトは対話型で、キーボード入力が必要なため、非対話型では実行できません。ユーザー自身がターミナルで実行するよう伝えてください。Bash ツールで実行しようとしてはいけません。
# macOS / Linux
bash scripts/scaffold.sh
# Windows
.\scripts\scaffold.ps1
ユーザーが(スクリプトを実行せずに)プロジェクトを直接生成するよう求めた場合は、ステップ3に進み、5ブロックパターンに従ってファイルを手動で作成してください。
Kotlin + Gradle — 必須の最小ファイルセット:
build.gradle.kts — 省略すると毎回ビルドに失敗する項目が3つあります。
plugins {
kotlin("jvm") version "2.0.21" // ① must come BEFORE the Gatling plugin
id("io.gatling.gradle") version "3.14.5"
}
group = "perf"
version = "1.0.0-SNAPSHOT"
repositories {
mavenCentral() // ② required — often forgotten
}
gatling {
jvmArgs = listOf(
"-Xms512m", "-Xmx2g",
"--add-opens=java.base/java.lang=ALL-UNNAMED" // ③ Java 21+ module restriction
)
}
dependencies {
gatling("io.gatling.highcharts:gatling-charts-highcharts:3.14.5")
}
ソースディレクトリ(Gatling Gradle プラグインでは src/test/ ではありません):
- Kotlin シミュレーション →
src/gatling/kotlin/<package>/ - リソース(gatling.conf、フィーダー)→
src/gatling/resources/
JS/TS プロジェクトの場合、必須の最小ファイルセットは次のとおりです。
"type": "module"と依存関係@gatling.io/cli、@gatling.io/core、@gatling.io/httpを含むpackage.jsonsrc/<SimulationName>.gatling.js(または.gatling.ts) — ファイルは CLI で必要とされる.gatling.js/.gatling.tsのサフィックスを付けて、src/直下に置く必要がありますtsconfig.json— TypeScript のみ。esbuild-plugin-tscで必要です。省略するとバンドル時にfailed to open 'undefined'が発生します
スキャフォールディング後、またはユーザーが既存の Gatling プロジェクトを共有した場合は、Bash ツールでバリデーターを実行してください(ファイルの中身は読まず、出力だけを確認してください。内容を読むとトークンを消費します)。
bash scripts/validate.sh [project-dir]
結果を報告してください。このスクリプトは、ユーザーが壊れた実行で時間を無駄にする前に、よくある設定ミス5つを検出します。
ステップ3 — 5ブロックパターンを適用する
すべてのシミュレーションには、次の5つのブロックをこの順序で含める必要があります。まず選択した言語で完全な骨格を生成し、その後詳細を埋めてください。部分的なファイルから始めると、構造上のエラーにつながります。
Block 1 → Protocol baseUrl, headers, connection settings
Block 2 → Feeders test data injected per virtual user
Block 3 → Scenario ordered chain of requests with pauses and checks
Block 4 → Injection how many users, at what rate, for how long
Block 5 → Assertions pass/fail thresholds (success rate, response time p95)
よくあるミス — すべてのシミュレーションで確認する
以下の5つのエラーは、Gatling シミュレーションの初稿でほぼ必ず見られます。コードを提示する前に確認してください。
1. リクエスト間の pause() がない
思考時間がないと、すべてのリクエストが可能な限り最大の速度で送信され、実際のユーザーの10〜100倍の負荷が発生します。その結果は意味をなさなくなり、テスト対象のシステムがクラッシュするおそれもあります。
// Wrong
scenario("Flow").exec(http("A").get("/a")).exec(http("B").get("/b"))
// Correct — add realistic think time between actions
scenario("Flow").exec(http("A").get("/a")).pause(1, 3).exec(http("B").get("/b"))
2. ハードコードされた動的トークン
ハードコードされたトークンを使うと、すべての仮想ユーザーが同じセッションを送信します。つまり、サーバーからは多数の異なるユーザーではなく、同じユーザーが繰り返しアクセスしているように見えます。CSRF トークンと JWT はサーバー側で検証されるため、実際のログインレスポンスから取得する必要があります。
// Wrong — static token shared across all users
.header("Authorization", "Bearer eyJhbGciOiJIUzI1NiJ9.abc123")
// Correct — extract per user from the login response
.exec(http("Login").post("/auth/login")
.check(jsonPath("$.token").saveAs("token")))
.exec(http("API Call").get("/data")
.header("Authorization", "Bearer #{token}"))
3. 負荷テストでの atOnceUsers
atOnceUsers はすべてのユーザーを同時に開始します。これはスモークテスト(2〜5 ユーザー)にのみ適しています。実際の負荷テストに使用すると、現実とは異なるスパイクが発生し、キャパシティについて何も分かりません。
// Wrong — not a load test, just a spike
setUp(scn.injectOpen(atOnceUsers(100)))
// Correct — ramp up, then hold to measure steady-state capacity
setUp(scn.injectOpen(
rampUsers(100).during(60),
constantUsersPerSec(10).during(120)
))
4. アサーションがない
アサーションがないと、すべてのリクエストが 500 を返していても、Gatling はコード 0(成功)で終了します。つまり、CI/CD パイプラインでパフォーマンスのリグレッションを検出できません。テストを実行する前に、「成功」とは何かを定義してください。
// Wrong — always exits 0 regardless of results
setUp(scn.inject(...).protocols(httpProtocol))
// Correct — fail the build if thresholds are breached
setUp(scn.inject(...).protocols(httpProtocol))
.assertions(
global().successfulRequests().percent().gt(99.0),
global().responseTime().percentile(95).lt(1000)
)
5. 長時間テストでの .queue() フィーダー戦略
.queue() は各 CSV レコードを順番に一度だけ消費します。ファイルの内容がなくなると、テストは実行中に失敗します。レコード数で許される時間より長く実行される可能性があるテストでは、.circular() を使用してください。
// Wrong — crashes when file is exhausted
FeederBuilder<String> f = csv("data/users.csv").queue()
// Correct for sustained tests — loops back to the start
FeederBuilder<String> f = csv("data/users.csv").circular()
// Use .queue() only when each record must be unique (e.g., user registration)
6. Check に失敗するとリクエストが FAILED としてマークされる
対象が見つからない .check() は、サーバーが 200 を返した場合でも、リクエスト全体を失敗させます。これにより、エラー率が気づかれないまま上昇し、フィールドが存在しなかった、またはパスが間違っていたという本当の問題が隠れてしまいます。
// Wrong — if $.token is absent (e.g., login failed), request is marked FAILED
.check(jsonPath("$.token").saveAs("token"))
// Correct — validate existence first so the error message is meaningful
.check(status().is(200))
.check(jsonPath("$.token").exists())
.check(jsonPath("$.token").saveAs("token"))
// When the field is genuinely optional — use .optional() to avoid false failures
.check(jsonPath("$.refreshToken").optional().saveAs("refreshToken"))
7. リクエストボディ送信時の Content-Type の欠落
POST/PUT リクエストで Content-Type を忘れると、サーバーは 415 Unsupported Media Type で拒否します。.asJson() を使用すると、1 回の呼び出しで Content-Type と Accept ヘッダーの両方が設定されます。
// Wrong — server returns 415
.post("/api/users").body(StringBody("""{"name":"#{name}"}"""))
// Correct — use .asJson() shorthand (Java / Kotlin only)
.post("/api/users").body(StringBody("""{"name":"#{name}"}""")).asJson()
// Equivalent explicit form
.post("/api/users")
.header("Content-Type", "application/json")
.body(StringBody("""{"name":"#{name}"}"""))
Scala に関する注意: Scala では .asJson() を .body() の後につなげることはできません。コンパイルエラーになります。プロトコル側で Content-Type を設定してください。
// Scala — set contentTypeHeader on the protocol, not per-request
val httpProtocol = http
.baseUrl(sys.props.getOrElse("baseUrl", "https://api.example.com"))
.acceptHeader("application/json")
.contentTypeHeader("application/json") // applies to all requests
// Then just use .body() without .asJson():
http("POST Login").post("/api/auth/login")
.body(StringBody("""{"email":"#{email}","password":"#{password}"}"""))
.check(status.is(200))
5 ブロックパターン — リファレンス
ブロック 1:プロトコル
// Java / Kotlin
HttpProtocolBuilder httpProtocol = http
.baseUrl(System.getProperty("baseUrl", "https://api.example.com"))
.acceptHeader("application/json")
.contentTypeHeader("application/json");
// Scala
val httpProtocol = http
.baseUrl(sys.props.getOrElse("baseUrl", "https://api.example.com"))
.acceptHeader("application/json")
// TypeScript / JavaScript
// File must be named *.gatling.ts / *.gatling.js and placed directly in src/
import {
simulation, scenario, rampUsers, csv, global,
StringBody, jsonPath, getEnvironmentVariable, // ← jsonPath and StringBody live here, NOT in @gatling.io/http
} from "@gatling.io/core";
import { http, status } from "@gatling.io/http"; // ← do NOT import jsonPath from here — it is not exported
// process.env is NOT available at GraalVM runtime — use getEnvironmentVariable instead
const httpProtocol = http
.baseUrl(getEnvironmentVariable("BASE_URL", "https://api.example.com"))
.acceptHeader("application/json");
ブロック 2:フィーダー
csv("data/users.csv").circular() // sustained tests: loops forever (recommended)
csv("data/users.csv").random() // picks records randomly, allows repeats
csv("data/users.csv").queue() // each record used once — only for unique data
csv("data/users.csv").shuffle() // random order, each used once
// Programmatic feeder — when each user needs a unique generated value
Iterator<Map<String, Object>> feeder =
Stream.generate(() -> Map.<String, Object>of("id", UUID.randomUUID().toString()))
.iterator();
ブロック 3:シナリオ
// Java
ScenarioBuilder scn = scenario("My Flow")
.feed(userFeeder)
.exec(http("POST Login")
.post("/auth/login")
.body(StringBody("""{"username":"#{username}","password":"#{password}"}"""))
.check(status().is(200))
.check(jsonPath("$.token").saveAs("token"))) // extract token for reuse
.pause(1, 3) // think time
.exec(http("GET Data")
.get("/data")
.header("Authorization", "Bearer #{token}") // inject extracted token
.check(status().is(200))
.check(jsonPath("$.id").saveAs("resourceId")))
.pause(1)
.exec(http("POST Action")
.post("/actions")
.header("Authorization", "Bearer #{token}")
.body(StringBody("""{"resourceId":"#{resourceId}"}"""))
.check(status().is(201)));
// TypeScript
const scn = scenario("My Flow")
.feed(userFeeder)
.exec(http("POST Login").post("/auth/login")
.body(StringBody('{"username":"#{username}","password":"#{password}"}')) // ← StringBody required; raw string causes NullPointerException
.asJson() // ← sets Content-Type: application/json
.check(status().is(200))
.check(jsonPath("$.token").exists())
.check(jsonPath("$.token").saveAs("token")))
.pause(1, 3)
.exec(http("GET Data").get("/data")
.header("Authorization", "Bearer #{token}")
.check(status().is(200)));
ループと条件分岐:
repeat(3).on(exec(http("Poll").get("/status"))) // fixed iterations
during(Duration.ofSeconds(30)).on( // time-based loop
exec(http("Ping").get("/ping")).pause(5))
doIf("#{isPremium}").then(exec(http("VIP").get("/vip"))) // conditional branch
randomSwitch().on( // weighted paths
percent(60.0).exec(http("Browse").get("/products")),
percent(40.0).exec(http("Search").get("/search")))
group("Checkout Flow").on( // group for cleaner reports
exec(http("Cart").get("/cart"))
.exec(http("Pay").post("/pay")))
ブロック 4:インジェクションプロファイル
テストの目的に合ったプロファイルを選んでください。誤ったプロファイルを使うと、誤解を招く結果になります。
injectOpen — 到着率(1 秒あたりの新規ユーザー数)を制御します。Web API やステートレスサービスでのデフォルトです。
| プロファイル | コマンド | 使用場面 |
|---|---|---|
| スパイク(スモークテストのみ) | atOnceUsers(5) | テストが実行されることを確認する場合。負荷テストではありません |
| ランプアップ | rampUsers(100).during(60) | 標準的な負荷テスト |
| 安定したレート | constantUsersPerSec(20).during(120) | キャパシティ/ソークテスト |
| 加速 | rampUsersPerSec(5).to(50).during(60) | 限界点を見つける場合 |
| ストレスピーク | stressPeakUsers(500).during(30) | ストレステスト |
| 段階的増加 | incrementUsersPerSec(5).times(5).eachLevelLasting(30) | キャパシティを段階的に確認 |
injectClosed — 同時接続数(同時にアクティブなユーザー数)を制御します。接続プール、キュー、またはセッション数の上限があるシステムで使用してください。
| プロファイル | コマンド | 使用場面 |
|---|---|---|
| 同時接続数一定 | constantConcurrentUsers(50).during(120) | 接続プールのサイズが固定されている場合 |
| 同時接続数のランプアップ | rampConcurrentUsers(10).to(50).during(60) | 同時接続数を段階的に増やす場合 |
| 同時接続数の段階的増加 | incrementConcurrentUsers(5).times(5).eachLevelLasting(30) | キャパシティを段階的に確認(クローズド) |
Scala に関する注意: .inject(...) を使用してください。Scala では呼び出し箇所に injectOpen/injectClosed の区別はありません。ステップの型によってモデルが決まります。
スロットリング — ユーザー数にかかわらず RPS を制限します:
固定ユーザー数ではなく、固定リクエストレートでテストする場合は .throttle() を使用してください。インジェクション設定を上書きするため、SLA 準拠テストに役立ちます。
setUp(scn.injectOpen(constantUsersPerSec(50).during(Duration.ofMinutes(10))))
.throttle(
reachRps(100).in(Duration.ofSeconds(10)), // ramp to 100 RPS over 10s
holdFor(Duration.ofMinutes(5)) // hold at 100 RPS for 5 min
)
.protocols(httpProtocol);
一時停止の分布 — 必要なリアリティに応じて選択してください:
.pause(1, 3) // uniform: between 1-3s (default)
.pause(Duration.ofSeconds(2),
PauseType.EXPONENTIAL) // exponential: closer to real user behavior
.pace(Duration.ofSeconds(5)) // cadence: fixed cycle regardless of response time
ブロック 5:アサーション
アサーションを設定すると、テストが合格/不合格を判定するゲートになります。アサーションがなければ、テストは単なる観測にすぎません。最低限、最初の行を含めてください。重要な経路については、エンドポイントごとにアサーションを追加します。
.assertions(
global().failedRequests().count().lt(1L), // minimum: zero errors
global().successfulRequests().percent().gt(99.0), // success rate
global().responseTime().percentile(95).lt(1000), // p95 < 1s
global().responseTime().percentile(99).lt(2000), // p99 < 2s
global().requestsPerSec().gt(50.0), // throughput floor
details("POST Login").responseTime().percentile(99).lt(500) // per-endpoint
)
mean() ではなく、percentile(95) と percentile(99) を使用してください。平均値では裾野が隠れてしまいます。平均が 200ms の場合、p99 が 10 秒でも見えません。
Kotlin に関する注意: percentile() には Double 引数が必要です。.percentile(95).lt(1000) ではなく、.percentile(95.0).lt(1000) と記述してください(Java では型の拡大変換によって受け入れられますが、Kotlin では Int によってコンパイルエラーになります)。
Scala に関する注意: アサーションメソッド(global、responseTime、successfulRequests、percent)は、暗黙の GatlingConfiguration 引数を持つ引数なしメソッドです。括弧を付けずに呼び出してください。Scala では global() はコンパイルエラーになります。
// Wrong — compile error: "not enough arguments for method global"
global().successfulRequests().percent().gt(99.0)
// Correct — no () on zero-arg methods with implicit params
global.successfulRequests.percent.gt(99.0)
global.responseTime.percentile(95).lt(800)
details("POST Login").responseTime.percentile(99).lt(300)
実行コマンド
# Maven
mvn gatling:test -Dgatling.simulationClass=perf.MySimulation \
-DbaseUrl=https://staging.example.com -Dusers=50
# Gradle — plugin 3.14+ supports Gradle 9; use ./gradlew (or gradle) directly
./gradlew gatlingRun # runs all simulations
./gradlew gatlingRun-perf.MySimulation # specific class (plugin ≥ 3.14 only)
# TypeScript / JavaScript (use simulation name, not file path)
BASE_URL=https://staging.example.com USERS=50 \
npx gatling run --simulation MySimulation
レポートは次の場所に表示されます:target/gatling/<simulation>-<timestamp>/index.html