12k
All articles

スモークテストと、エージェントがそれを書き続ける理由

Smokeテストの要点を解説。何を確認し、どこで実行し、何を除外するか、そしてコードエージェントがCIやデプロイに追加し続ける理由。

OpenReplay Team
OpenReplay Team
スモークテストと、エージェントがそれを書き続ける理由

スモークテストとは、デプロイされたシステムが根本的に「生きている」ことを確認する小さなチェックの集まりです。プロセスが起動したか、メインページが200を返すか、ユーザーがログインできるか、データベースがクエリに応答するか。ソフトウェアが正しいかどうかを判定するものではなく、残りのテストスイートを実行する価値があるかどうかを判断するものです。

長年CIに出荷してきたのに、この定義を必要としたことが一度もないという方も少なくないでしょう。この用語は前置きなしに登場することが多く、最近ではプルリクエストの中に現れます。コーディングエージェントが別の作業を仕上げる途中で追加した smoke.shsmoke.spec.ts として。

この記事では、スモークテストに含めるべきもの、どこで実行するのか、最小構成のコード例、含めないものを判断するフィルタ、そしてなぜエージェントがこれほど確実にスモークテストを生成するのかを取り上げます。

重要なポイント

  • スモークテストは、デプロイされたシステムが生きていること(起動、メインページの200、ログイン、実際のデータベース読み取り1件)を確認するもので、所要時間は分単位ではなく秒単位。
  • 実行タイミングは2回。CIにおける最初のゲートとして、そしてデプロイ直後に実際にデプロイされた環境に対して。localhostに対する実行ではデプロイの失敗を捕捉できない。
  • あるチェックがスモークスイートに属するのは、その失敗が全ユーザーをブロックし、全ユーザーがそこを通過し、デプロイ時に壊れうる場合のみ。3つすべてを満たさなければリグレッションスイート行き。
  • コーディングエージェントがスモークテストを書くのは、完了したタスクに対して「根本的なものは何も壊れていない」という安価でバイナリかつ高速なシグナルが必要だからであり、それはまさにスモークテストが生み出すものだから。
  • スイートに厳格な上限を設け、それを超えるものはリグレッションへ移すこと。12分かかるスモークスイートは、名前を間違えたリグレッションスイートにすぎない。

スモークテストとは何か

スモークテストは「このビルドは、さらにテストを進めるに足るだけ生きているか?」という一つの問いに答えるものです。その名称は一般に、新しいハードウェアに通電して煙が出ないか観察することに由来すると言われています。ソフトウェアでも形は同じで、すべてのユーザーが依存するパスを高速かつ浅く一巡し、結果はバイナリで、正しさについては一切の見解を持ちません。

これは通常のテストピラミッドの一層に位置するのではなく、その外側に立つものです。各層については Integration Tests vs End-to-End Tests および Unit vs Integration Testing in JavaScript: What to Use When で解説しています。スモークテストはそれらのスイートの前に立つゲートであって、その一員ではありません。

スモークテストはどこで実行するのか

スモークテストは2箇所で実行します。より長いスイートが始まる前のCIにおける最初のゲートとして、そしてデプロイ直後に、実際にデプロイされた環境に対して。真価を発揮するのは2番目の実行タイミングですが、これは最も省略されがちでもあります。

localhostに対してスモークテストを実行するのは本来の目的を無にします。なぜなら、スモークテストが捕捉するために存在する失敗はデプロイ時にしか発生しないからです。環境変数の欠落、実行されなかったマイグレーション、出荷されなかったアセットバンドルなどです。CIランナー上で APP_URL=localhost と新規のインメモリデータベースで起動したプロセスには、こうした障害モードがそもそも存在しません。そのような実行は起動チェック(boot check)であり、起動チェックは有用ではあるものの、別物であり、より弱いものです。モック化されたサービスに対するローカル実行は、デプロイが失敗する理由のいずれによっても失敗しえないため、スモークテストという名前を名乗るべきではありません。

デプロイ後の実行は、ロールバックの自然なトリガーでもあります。AWS CodeDeploy は、新バージョンがテストトラフィックを処理し始めた時点で検証用関数を実行し、その結果が失敗であればロールバックを起動します。ただし、このシグナルの限界は念頭に置いてください。デプロイ後のスモークテストがグリーンであることは、システムが応答したことを証明するだけで、ユーザージャーニーが機能することを証明するものではありません。両者を切り分ける手法は、リリース後の最初の実セッションのセッションリプレイを確認することです。変更されたチャンクハッシュで例外を投げるチェックアウトボタンは、ステータスコードには決して現れないからです。

スモークテストはどのようなものか

完全なスモークスイートは、モックを一切使わず実際のデプロイ先に対して実行される1本の短いスクリプトで済みます。ヘルスエンドポイントへの時間制限付きの待機、認証付きリクエスト1件、そしてアプリケーションを経由してデータベースに到達する読み取り1件です。

#!/usr/bin/env bash
# smoke.sh: runs against the deployed target in $APP_URL, never localhost
set -euo pipefail

: "${APP_URL:?set APP_URL to the deployed base URL}"
: "${SMOKE_USER:?}" "${SMOKE_PASS:?}"

status() { curl --silent --output /dev/null --write-out '%{http_code}' "$@"; }

# 1. Wait for the process to come up. This absorbs container start-up, nothing else.
for _ in $(seq 1 "${SMOKE_RETRIES:-10}"); do
  [ "$(status "$APP_URL/health")" = "200" ] && break
  sleep 3
done
[ "$(status "$APP_URL/health")" = "200" ] || { echo "health: not 200"; exit 1; }

# 2. Login. Assert the one status your app returns (200 for a JSON API, 302 for a form post).
code=$(status --data-urlencode "email=$SMOKE_USER" \
              --data-urlencode "password=$SMOKE_PASS" "$APP_URL/login")
[ "$code" = "200" ] || { echo "login: got $code, expected 200"; exit 1; }

# 3. A real read through the app, with the credentials the app was deployed with.
body=$(curl --silent --fail "$APP_URL/api/products?limit=1") || { echo "query: request failed"; exit 1; }
[ -n "$body" ] || { echo "query: empty body"; exit 1; }

echo "smoke: ok"

status ヘルパーは curl の --write-out '%{http_code}' でレスポンスコードを取得し、--output /dev/null でボディを破棄します。set -euo pipefail により、スクリプトは最初の失敗で終了します。リトライループはデプロイ後の起動時間を吸収するためだけに存在しており、不安定(flaky)なチェックを取り繕う手段ではありません。

各アサーションは正確なステータスを1つだけ指定しています。RFC 9110 のすべてのコードには意味があるため、「何らかのレスポンス」を受け入れるチェックはチェックとは言えません。存在するはずのルートからの404はデプロイの失敗であり、302が許容されるのは契約上リダイレクトが期待される場合だけです。3番目のチェックは見た目以上に重要です。pg_isready のようなツールでデータベースサーバーにpingを送れば、サーバーが接続を受け付けることは確認できます。しかしアプリケーションを経由した読み取りは、アプリがデプロイ時に与えられた接続文字列・認証情報・スキーマでデータベースに到達できることを確認するものであり、これこそがスモークテストが存在する理由である障害クラスです。

スモークテストに含めるべきでないもの

エッジケース、ビジネスロジック、遅いもの、そして不安定なものは、スモークテストに含めるべきではありません。あるチェックがスモークスイートに属するのは、次の3条件がすべて成立する場合のみです。その失敗によって全ユーザーが何もできなくなること、全ユーザーがそこを通過すること、そしてデプロイ中に壊れうること。3つ未満しか満たさないチェックはリグレッションスイートに属します。この3つの問いによる整理は、少なくとも1社のCIテストベンダーのガイダンスに見られるもので、標準というよりヒューリスティックです。

候補となるチェック全ユーザーをブロックするか?全ユーザーが通過するか?デプロイで壊れるか?判定
ヘルスエンドポイントが200を返すYesYesYesスモーク
テストアカウントでのログインYesYesYesスモーク
クーポンコードで割引が適用されるNoNoYesリグレッション
管理者向けCSVエクスポートのダウンロードNoNoYesリグレッション
パスワードリセットメールが届くNoNoYesリグレッション

不安定さ(flakiness)は、それ単独で失格要件です。ランダムに失敗するスモークチェックは、レッドになったゲートを再実行するという習慣をチームに教え込みます。通るまで再実行されるゲートは、もはやゲートではありません。

なぜコーディングエージェントはスモークテストを書き続けるのか

コーディングエージェントがスモークテストを書くのは、タスクを完了しようとするエージェントが「システム全体を壊していない」という安価で高速かつ曖昧さのないシグナルを必要とするからであり、それはまさにスモークテストが生み出すシグナルだからです。コードを編集し終えたばかりのエージェントは、反復のたびにフルスイートを走らせる余裕はなく、目視で正しさを判断することもできません。そのため、「まだ生きているか」に数秒で答え、クリーンな終了コードを返すチェックに手を伸ばすのです。

これは偶然ではありません。AnthropicのClaude Codeのドキュメントは、エージェントが自分の作業を確認するために実行できるものを渡すよう開発者に勧めています。それがテストスイートであれ、ビルドであれ、リンターであれ、小さなスクリプトであれ構いません。自分で読み取れるシグナルが与えられれば、エージェントは人間が誤りに気づくのを待たずに作業と再確認を続けられます。スモークスクリプトはこの説明にぴたりと当てはまり、だからこそエージェントが書いたリポジトリは、チームがこの用語を一度も使っていなくてもスモークスクリプトを育てていく傾向があります。開発者が今「スモークテスト」に出会っているのは、多くの場合、自分が書いていないdiffの中にそれを見つけるからです。

PRにスモークテストが現れたら、次の4つの問いで確認してください。localhostをハードコードするのではなく、対象URLを環境変数から読んでいるか?「エラーでないこと」ではなく特定のステータスをアサートしているか?数秒で完了するか?各チェックは上記の3つの問いによるフィルタを通過するか?これらのいずれかを満たさないエージェント作成のスモークテストは、起動チェックか、間違ったラベルを貼ったリグレッションテストのどちらかです。

失敗パターン:スイートの肥大化

スモークスイートは、数秒を超えて肥大化した瞬間にゲートでなくなります。パターンは予測可能です。機能が追加されるたびに「念のため」チェックが増え、エージェントがタスクのたびにさらに追加し、ある日ゲートに12分かかるようになり、人々はそれをスキップし始めます。そうなればそれは、間違った名前を冠した遅いリグレッションスイートです。

対処法は、厳格な上限を設け、それを超えるものをすべてリグレッションへ移すことです。推奨するデフォルトは、数秒で完了する5件程度のわずかなチェックです。TestingXpertsは上限を10分としており、それを超えるとチームはゲートをスキップし始めるとしています。正確な数字よりも重要なのは、その基準を明文化し、エージェントによる追加分も含めてレビューで強制することです。

結論

スモークテストとは、デプロイが生きていることを示す最小限の証明です。ヘルス、ログイン、実際の読み取り1件を、実際に出荷した環境に対して実行し、数秒で完了する。それ以外はすべてリグレッションです。次にエージェントが smoke.sh を渡してきたら、実際のURLを対象にしているか、正確なステータスをアサートしているか、上限内に収まっているかを確認し、そのうえで毎回のデプロイ後に実行されるよう組み込んでください。

FAQ

スモークテストとヘルスチェックの違いは何ですか?

ヘルスチェックは、オーケストレーターやロードバランサーがトラフィックをルーティングするかコンテナを再起動するかを判断するためにポーリングする単一のエンドポイントです。Kubernetesではこれをlivenessプローブおよびreadinessプローブと呼びます。スモークテストはデプロイごとに1回実行され、そのエンドポイントに加えてログインとデータベース読み取りを呼び出し、パイプラインをゲートする終了コードを返します。ヘルスエンドポイントはスモークテストの最初のアサーションであって、その代替ではありません。

スモークテストとサニティテストの違いは何ですか?

スモークテストは広く浅いものです。より深いテストを開始する前に、ビルドのコアパスが生きていることを確認します。サニティテストは従来のQA用語では狭く深いものです。スモークを通過したビルド上で特定の修正や変更が機能することを検証し、多くの場合リグレッションテストのサブセットとみなされます。CIパイプラインにおいては、スモークスイートがゲートであり、サニティチェックはリグレッションに属します。

スモークテストは本番環境に対して実行すべきですか。それは安全ですか?

はい。デプロイ後の実行は、本番を含め、ユーザーが実際に到達する環境を対象とすべきです。デプロイの失敗はそこでしか現れないからです。安全に保つには、CIシークレット経由で渡す専用のシード済みテストアカウントを使い、チェックをログインと読み取り専用リクエストに限定し、データを書き込むものやメールを送信するものを除外してください。書き込みが避けられない場合は、テスト用テナントにスコープを限定し、同じスクリプト内でクリーンアップしてください。

シェルスクリプトの代わりにPlaywrightやCypressでスモークテストを書けますか?

はい。同じルールに従う限り可能です。ベースURLを環境変数から読み、正確なステータスまたは可視要素をアサートし、数秒で完了すること。ブラウザランナーはチェックごとに起動時間とページ読み込み時間が加わるため、ブラウザによるスモークスイートは1〜2本のジャーニーに留め、HTTPチェックはcurlに任せてください。スモークのspecは独立したファイルに置き、CIが他のスイートなしで実行できるようにしましょう。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.