mcpField
G
gatling-best-practices

gatling-best-practices

github.com/rcampos09测试与质量1164 次安装32 周前更新
代码仓库

关于 gatling-best-practices

gatling-best-practices 是用于编写和审查 Gatling 仿真的技能,支持 Java、Kotlin、Scala、JavaScript 和 TypeScript。它采用统一的五部分结构,并检查常见配置与负载测试错误。

主要功能

  1. 01支持五种 Gatling 语言
  2. 02采用五部分仿真结构
  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 场景构建器

为所有五种官方支持的语言强制采用一致、可用于生产环境的 Gatling 仿真模式:Java、Kotlin、Scala、JavaScript 和 TypeScript。

输出格式

生成或修复仿真时,始终提供以下三项:

  1. 一份完整、可运行的仿真文件,使用所选语言编写,绝不能只给出部分代码片段。用户应能直接复制并立即运行。
  2. 准确的运行命令,以及所需的环境参数(baseUrl、users 等)。
  3. 一行说明,解释所选注入配置及其如何满足指定的负载目标。

步骤 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。 其中包含模块化分层模式(Config → Requests → Scenarios → Simulations),以及 Java、Scala 和 TypeScript 示例。

步骤 2 — 新项目?使用脚手架脚本

脚手架脚本是交互式的——需要键盘输入,无法以非交互方式运行。请告知用户在自己的终端中运行脚本;不要尝试使用 Bash 工具执行:

# macOS / Linux
bash scripts/scaffold.sh

# Windows
.\scripts\scaffold.ps1

如果用户要求你直接生成项目(不运行脚本),请跳到步骤 3,按照 5 块模式手动创建文件。

Kotlin + Gradle — 必需的最少文件:

build.gradle.kts — 有三项遗漏会导致每次构建失败:

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、feeder)→ src/gatling/resources/

对于 JS/TS 项目,必需的最少文件包括:

  • package.json,其中包含 "type": "module" 以及依赖项 @gatling.io/cli、@gatling.io/core、@gatling.io/http
  • src/<SimulationName>.gatling.js(或 .gatling.ts)——文件必须直接位于 src/ 中,并使用 .gatling.js / .gatling.ts 后缀(CLI 要求)
  • tsconfig.json——仅限 TypeScript,esbuild-plugin-tsc 必需;省略后会在打包时导致 failed to open 'undefined'

完成脚手架搭建后——或用户分享现有 Gatling 项目时——使用 Bash 工具执行验证器(不要读取验证器文件,只有输出会消耗 token):

bash scripts/validate.sh [project-dir]

报告结果。该脚本会捕获五种最常见的配置问题,避免用户在运行失败的项目上浪费时间。

步骤 3 — 应用 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)

常见错误 — 检查每个仿真

这五种错误几乎出现在所有 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() feeder 策略

.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. 检查失败会将请求标记为 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()——一次调用即可设置 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:Feeders
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——控制到达速率(每秒新增用户数)。适用于 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
Block 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
)

使用 percentile(95) 和 percentile(99),而不是 mean()。平均值会掩盖长尾:当平均值为 200ms 时,p99 为 10 秒这一情况就会被隐藏。

Kotlin 说明: percentile() 需要一个 Double 参数——写成 .percentile(95.0).lt(1000),而不是 .percentile(95).lt(1000)(在 Kotlin 中,Int 会导致编译错误,尽管 Java 接受通过拓宽转换传入的值)。

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


参考资料

评论

支持 Markdown