mcpField
G
gatling-best-practices

gatling-best-practices

github.com/rcampos09テスト・品質1164 回インストール32 週間前に更新
リポジトリ

gatling-best-practices について

gatling-best-practicesは、Java、Kotlin、Scala、JavaScript、TypeScriptでGatlingシミュレーションを作成・レビューするスキルです。5ブロック構成を適用し、設定や負荷テストで起きやすいミスを確認します。

主な機能

  1. 015つのGatling言語に対応
  2. 025ブロックのシミュレーション構成
  3. 03テスト目標に応じた注入プロファイル選択
  4. 04よくあるシミュレーションの誤りを確認
  5. 05実行コマンドと検証方法の提示

活用シーン

  1. 01負荷テスト用シミュレーションの作成
  2. 02既存Gatlingプロジェクトのレビュー
  3. 03テスト目標に合う注入プロファイルの選択
  4. 04実行前の設定問題の発見

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/rcampos09/performance-testing-skills --skill gatling-best-practices

インストール後、Claude Code で/gatling-best-practicesを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

evals/
evals.json
references/
DESIGN-PATTERNS.md
PROTOCOLS.md
scripts/
scaffold.ps1
scaffold.sh
validate.sh

詳細

Gatling シナリオビルダー

Java、Kotlin、Scala、JavaScript、TypeScriptの、公式にサポートされている5つの言語すべてで、一貫性があり、本番環境に対応した Gatling シミュレーションのパターンを徹底します。

出力形式

シミュレーションを作成または修正するときは、必ず次の3つを提示してください。

  1. 選択した言語で完全に実行可能なシミュレーションファイル。部分的なスニペットではなく、ユーザーがコピーしてすぐに実行できるものにしてください。
  2. 環境パラメーター(baseUrl、usersなど)を含む正確な実行コマンド。
  3. 選択したインジェクションプロファイルと、指定された負荷目標に適している理由を説明する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.json
  • src/<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


参考資料

コメント