Claude Media
Claude CodeでLocust負荷テストのシナリオを書く手順

Claude CodeでLocust負荷テストのシナリオを書く手順

Claude Codeにシナリオ設計とlocustfile.pyの実装を任せ、Web UIでの確認からヘッドレス実行・CI組み込みまでを手順化します。

Claude CodeにLocustのシナリオ設計とlocustfile.pyの実装を任せると、Web UIでの動作確認からヘッドレス実行の設定までを一貫した手順で進められます。ポイントは、シナリオ設計・実装・検証を別々の指示に分けて進めることです。この記事では、インストールからCIで使うヘッドレスモードの設定までを手順化します。

Locustは専用DSLではなくPythonのプログラム

Locustの負荷テストは専用のDSLではなく、通常のPythonプログラムです。公式ドキュメントも「テスト対象のシステムへリクエストを送るだけのPythonプログラム」と説明しています。仮想ユーザーはgeventのグリーンスレッド(greenlet)として並行実行されるため、複雑なユーザーフローも1つのPythonクラスとして書けます。

この性質はClaude Codeとの相性がいい領域です。シナリオ設計(どのエンドポイントを、どんな比率で、どんな待ち時間で叩くか)と、locustfile.pyへの実装は別の指示に分けます。既存コードにテストを足すときの一般的な進め方と同じ型に落とし込めるからです。Claude Codeに既存コードのテストを書かせる手順でも、対象の特定→雛形生成→ケース追加→実行して直す、という順序が精度を左右すると述べています。Locustのシナリオ設計でも、この順序がそのまま有効です。

locustfileに最低限必要なもの

locustfile(既定のファイル名はlocustfile.py)は、Locustが読み込む負荷テストの定義ファイルです。有効なlocustfileになる条件は、Userを継承したクラスが1つ以上あることです。HTTPのテストならUserを拡張したHttpUserを継承し、@taskを付けたメソッドを1つ書けば動きます。

-fを省くと、カレントディレクトリのlocustfile.pyが読み込まれます。ブラウザの操作記録(HARファイル)から雛形を作るhar2locustもあります。

始める前に確認すること

着手前に3点を確認しておきます。

1点目はPython環境とLocustのインストールです。

pip install locust
locust --version

2点目は、テスト対象のホストと代表的なエンドポイントです。Locustは-H(--host)でベースURLを指定し、以降のリクエストはそこからの相対パスで書きます。

3点目は、Bashコマンドの実行許可です。Claude Codeがlocustコマンドを何度も実行して結果を読むたびに確認が入ると、検証のテンポが落ちます。Bash(locust:*)のように対象コマンドを絞って許可リストに追加しておくと、以降は確認なしで実行と結果読み取りを任せられます。

手順1: シナリオをClaude Codeに設計させる

最初に、テストしたいユーザーフローと、タスクごとの実行比率をClaude Codeに伝えます。ファイル名や既存のAPIルーティング定義を指定すると、対象が絞られて提案が具体的になります。

routes/api.pyを見て、/api/products一覧取得と/api/cart追加をシナリオにしたLocustのUserクラスを設計して。一覧取得を3倍の頻度で叩く

Locustのタスクは@taskデコレータを付けたメソッドとして定義し、@task(3)のように整数を渡すと選ばれる頻度の比率になります。上の指示であれば、一覧取得のタスクに@task(3)、カート追加のタスクに@task(1)を割り当てる設計になります。タスク実行後の待機時間はwait_time属性で指定し、between(1, 5)なら1〜5秒のランダム待機、constant(1)なら常に1秒の固定待機です。秒間の実行回数を一定に保ちたい場合はconstant_throughput、一定間隔で1回ずつ実行したい場合はconstant_pacingも使えます。到着間隔をばらつかせたいときは、指数分布から待ち時間を取るpoissonもあります。

待ち時間はタスク単位で効く点に注意が必要です。constant_throughput(2)でもタスクの中で2回リクエストすれば、1ユーザーあたり毎秒4リクエストになります。指示文に目標RPSを書くときは、1タスクのリクエスト数も一緒に伝えます。

一般ユーザーとは別に、管理者操作のような特定の1件だけを独立して流したい場合はfixed_count属性が使えます。fixed_count = 1を設定したUserクラスは、weightが無視され、指定した数だけ通常のユーザーより先に生成されます。-uを増減させても、管理者役のユーザー数は変わりません。

設計どおりの比率になっているかは、通信を起こさずに確かめられます。--show-task-ratioを付けて起動すると、タスクの実行比率が表で出ます。@task(3)と@task(1)のクラスでは、次のように75%と25%になりました(Locust 2.46.6)。

locust -f locustfile.py --show-task-ratio
 Task ratio per User class
--------------------------------------------------------------------------------
 100.0      ShopUser
   75.0       list_products
   25.0       missing

比率の確認までをClaude Codeの指示に含めておけば、重みの付け間違いを負荷をかける前に潰せます。

手順2: locustfile.pyを生成させて、レスポンスを検証する

雛形が固まったら、実際のPythonコードを生成させます。ログイン処理はon_startメソッドに書くと、各仮想ユーザーの開始時に1度だけ実行されます。

from locust import HttpUser, task, between
 
class ShopUser(HttpUser):
    wait_time = between(1, 5)
 
    def on_start(self):
        self.client.post("/api/login", json={"username": "demo", "password": "demo"})
 
    @task(3)
    def list_products(self):
        with self.client.get("/api/products", catch_response=True) as response:
            if response.status_code != 200:
                response.failure(f"unexpected status: {response.status_code}")
 
    @task(1)
    def add_to_cart(self):
        self.client.post("/api/cart", json={"product_id": 1, "qty": 1})

self.clientはHttpUserが持つHttpSessionで、リクエストの成否や応答時間をLocustの統計に自動で記録します。既定ではHTTPステータスが400未満なら成功扱いです。catch_response=Trueとwith文を組み合わせると、response.failure()で任意の条件を失敗として扱えます。逆にresponse.success()を呼べば、404のような通常は失敗扱いのレスポンスを成功として記録することもできます。動的なIDを含むURLを叩くタスクではname引数を渡すと、統計上のエントリを1つにまとめられます。

こうした命名規則や検証の書き方は、プロジェクトごとに固まっていくものです。毎回同じ方針を説明し直したくない場合は、Claude CodeモノレポのテストをSKILL.mdで教える手順と同じ要領で、Locust用のSKILL.mdに規約をまとめておくと再現性が上がります。locustfile.py自体は通常のPythonモジュールです。シナリオが増えてきたらcommon/ディレクトリにログイン処理や設定を切り出し、import common.authのように読み込む構成にもできます。

手順3: Web UIで動作確認する

シナリオができたら、まずWeb UIで挙動を目視確認します。

locust -f locustfile.py -H https://staging.example.com

http://localhost:8089を開くと、同時ユーザー数とスポーンレート(1秒あたりに増やすユーザー数)を入力する画面が出ます。実行を開始すると、Chartsタブでリクエスト数(RPS)・応答時間・実行中ユーザー数の推移をリアルタイムで確認できます。応答時間が伸び続けてRPSが頭打ちになったら、対象システムが飽和点に達しているサインです。

ここで1つ注意点があります。HttpUserは実際のブラウザではないため、HTMLレスポンスを解析して画像やCSS・JSを自動では取得しません。ページの表示を丸ごと模したい場合は、必要な静的リソースへのリクエストをタスクに明示的に追加する必要があります。Cookieの保持だけは自動で行われます。

手順4: ヘッドレスモードでCI向けに実行する

Web UIでの確認が終わったら、--headlessフラグでUIなしの実行に切り替えます。

locust -f locustfile.py --headless -u 100 -r 10 -t 5m \
  -H https://staging.example.com \
  --csv results --html report.html

-u(--users)がピーク時の同時ユーザー数、-r(--spawn-rate)が1秒あたりのスポーン数、-t(--run-time)が実行時間です。--run-timeを省略すると、テストは無期限に実行され続けます。--csvを指定するとresults_stats.csvなど3種類のCSVが、--htmlを指定するとHTMLレポートがそれぞれ出力されます。既定ではタスクが実行途中でも即座に止まります。完了を待ちたいときは-s(--stop-timeout)で猶予秒数を指定します。分散実行ではマスター側だけに指定すれば足ります。ヘッドレスでも、実行中にwキーで1ユーザー、Wキーで10ユーザー増やせます(s・Sで減らす)。

CIで合否判定に使うには、終了コードを利用します。既定では失敗リクエストが1件でもあると終了コード1を返し、--exit-code-on-errorでその値を変えられます。@events.quittingイベントでenvironment.process_exit_codeを自分で設定すれば、失敗率や応答時間のしきい値をもとに合否を決められます。次の例は、失敗率が1%超、平均応答時間が200ms超、95パーセンタイルが800ms超のいずれかで終了コード1にします。

import logging
from locust import events
 
@events.quitting.add_listener
def _(environment, **kw):
    total = environment.stats.total
    if total.fail_ratio > 0.01:
        logging.error("failure ratio > 1%")
        environment.process_exit_code = 1
    elif total.avg_response_time > 200:
        logging.error("average response time > 200 ms")
        environment.process_exit_code = 1
    elif total.get_response_time_percentile(0.95) > 800:
        logging.error("95th percentile > 800 ms")
        environment.process_exit_code = 1
    else:
        environment.process_exit_code = 0

このファイルはlocustfile.pyに書いても、そこからimportされる別ファイルに置いても動きます。GitHub Actionsなら、次のようにジョブへ組み込めます。

- run: pip install locust
- run: locust -f locustfile.py --headless --run-time 5m -H https://staging.example.com

1台では負荷が足りない場合は、--masterと--workerで複数プロセス・複数マシンに分散できます。マスター側で--expect-workersを指定すると、指定数のワーカーが接続するまで開始を待機します。GitHub Actionsのようなホスト型CIだけでなく、Claude Code自身のセルフホスト環境からヘッドレス実行をトリガーする構成も作れます。その場合はClaude Codeセルフホスト環境をCIでE2Eテストするで扱うRunnerの構成が参考になります。

時間指定ではなく、独自の条件でテストを打ち切りたい場合はenvironment属性からrunner.quit()を呼び出せます。タスク内でself.environment.runner.quit()を実行すると、スタンドアロン実行では全体が、ワーカーノード上ではそのノードだけが停止します。エラー率が閾値を超えた時点で即座に打ち切りたいときなど、-tだけでは表現しにくい終了条件に使えます。

404を流すと終了コードはどうなるか

終了コードの挙動は、ローカルのサーバーに実際に流して確かめられます。/api/productsだけ200を返し、それ以外は404を返す小さなHTTPサーバーを立て、GET /api/cartを3対1の比率の「1」側で叩く別構成で実行した結果が次です(Locust 2.46.6、5ユーザー・5秒)。

数字

404を含むヘッドレス実行の結果

  • GET /api/cart の失敗率

    100%

    404はステータス400以上なので失敗扱い

  • 終了コード(既定)

    1

    失敗サンプルが1件でもあるとき

  • --exit-code-on-error 0

    0

    同じ実行でも成功扱いで終了

実行後の統計表ではGET /api/cartが全件失敗、GET /api/productsが全件成功で並び、Error reportにはGET /api/cart: HTTPError('404 Client Error: Not Found for url: /api/cart')のようにURLごとのエラーが集約されます。CIで「404を想定どおりの応答として許容する」シナリオを書くときは、catch_response=Trueでresponse.success()を呼ぶ必要があります。終了コードだけを--exit-code-on-error 0で潰すと、本物の失敗も合格になります。

Web UIとヘッドレス、どちらを使うか

くらべる

用途ごとの使い分け

調整・目視確認

Web UI

シナリオの初期調整向きです。グラフを見ながらユーザー数やスポーンレートをその場で変えられます。

CI・耐久・共有

ヘッドレス

引数だけで完結し、終了コードで合否を判定できます。長時間の耐久試験は--csvで統計をファイルに残し、チームへの共有は--htmlのレポート1ファイルで済みます。

よくあるつまずき

wait_timeの指定漏れは、想定より高い負荷を生みます。指定がない場合はタスクが終わるとすぐ次のタスクが実行されるため、実際のユーザーの操作間隔より高頻度でリクエストが飛びます。

--run-timeを省いたヘッドレス実行は止まりません。CI環境でこれを忘れると、ジョブがタイムアウトするまでLocustが動き続けます。CIの設定側にもタイムアウトを別途入れておくと二重に安全です。

nameを付け忘れた動的パラメータ付きリクエストは、統計が分散します。/item?id=1と/item?id=2が別エントリとして記録され、集計時の見通しが悪くなります。

複数のUserクラスを持つlocustfileは、コマンドラインでクラスを指定しない限り均等にスポーンされます。比率を変えたい場合は各クラスにweight属性を設定するか、locust -f locustfile.py WebUserのように対象クラスを明示します。

まとめ

CIで使うなら、合否の基準を終了コードに寄せるかどうかが最初の分かれ目です。失敗率と応答時間のしきい値を@events.quittingに書いてClaude Codeに実装させ、404のような想定内の応答はcatch_response側で個別に扱うと、誤って合格にする経路を減らせます。実行後にp95が跳ねたときの読み方はClaudeで負荷試験の結果を分析する手順にあります。

Claudeにテストケースの生成そのものを自動で広げさせる別のアプローチとして、ClaudeとProperty-Based TestingでPythonのバグを見つける仕組みもあります。Locustが負荷という軸を探索するのに対し、Property-Based Testingは入力ドメインを探索する手法です。負荷試験と合わせて、テストの網羅性を広げる選択肢として参考になります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. docs.locust.io/en/stable/quickstart.html
  2. docs.locust.io/en/stable/writing-a-locustfile.html
  3. docs.locust.io/en/stable/running-without-web-ui.html
  4. docs.locust.io/en/stable/configuration.html
この記事を共有:XはてブLinkedIn