コマンドのモックと外部ツールのスタブ化
PATHを上書きして偽のバイナリを定義し、実際のシステムに触れずにスクリプトをテストします。
「コマンドのモックと外部ツールのスタブ化」はCoddyKit上の無料Linux Command Line & Bash Scripting Masteryレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはLinux Command Line & Bash Scripting Mastery学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Linux Command Line & Bash Scripting Masteryコースには全4レッスンが含まれています。
Bashのテストでコマンドをモックする理由
curl、aws、gitなどの外部ツールを呼び出すBashスクリプトをテストすると、問題に直面します。実際の呼び出しではネットワークにアクセスしたり、状態を変更したり、費用が発生したりします。また、CI環境にそのツールがインストールされていないために失敗することもあります。
モックとは、実際のコマンドを、自分で制御できる偽物に置き換えることです。偽物(stub)は予測可能な出力と終了コードを返すため、テストを高速かつ分離された再現性のあるものにできます。
- ネットワークやクラウドへのアクセスが不要です
- テストを数秒ではなく数ミリ秒で実行できます
- 実際のシステムでは発生させにくいエラーをシミュレートできます
- CIパイプラインをクリーンかつ依存関係のない状態に保てます
Bashには、これを実現する驚くほどシンプルな仕組みがあります。実際のバイナリより前に検索される$PATH上の場所に、偽物のバイナリを置くだけです。
PATH検索の仕組み
シェルがcurlのようなコマンドを実行するとき、$PATHに含まれる各ディレクトリを左から右へ検索し、最初に見つかった一致ファイルを実行します。
つまり、自分で用意したcurlスクリプトを含むディレクトリを先頭に追加すれば、シェルが/usr/bin/curlまで到達することはありません。
上書きのパターン:
- 一時ディレクトリ(stub bin)を作成します
- 実際のコマンドと同じ名前の偽の実行ファイルを書き込みます
- そのディレクトリを
PATHの先頭に追加します - テスト対象のスクリプトを実行します。実際のバイナリではなく、用意したstubが呼び出されます
- テスト後に一時ディレクトリを削除します
root権限も、システムファイルの変更も、特別なフレームワークも必要ありません。
stubディレクトリを作成する
標準的なパターンでは、mktemp -dを使ってstub用の分離された一時ディレクトリを作成します。テストまたはテストスイートごとに専用のディレクトリを用意することで、テスト間の汚染を防げます。
テストが完了したら、rm -rfでディレクトリを削除します。trapを使えば、エラーによってテストが途中で終了した場合でも確実に後片付けできます。
#!/usr/bin/env bash
# Setup a stub bin directory for testing
# Create the temp dir
STUB_BIN=$(mktemp -d)
# Always clean up on exit (success, error, or signal)
trap 'rm -rf "$STUB_BIN"' EXIT
# Prepend it to PATH so our stubs take priority
export PATH="$STUB_BIN:$PATH"
echo "Stub bin: $STUB_BIN"
echo "PATH starts with: ${PATH%%:*}"
# Your tests would go here...
echo "Tests complete."最初のstubを書く
stubは、置き換えたいコマンドと同じ名前を持つ実行可能ファイルです。テスト対象のスクリプトが期待する出力を表示し、指定した終了コードで終了します。
stubに関する主なルール:
- ファイルは実行可能である必要があります(
chmod +x) - shebang行(
#!/usr/bin/env bash)が必要です - 実際のスクリプトが解析する出力をechoします
- 成功には
exit 0を使い、シミュレートする失敗にはゼロ以外の値を使います
#!/usr/bin/env bash
# Create a stub for 'curl' that returns a fake HTTP response
STUB_BIN=$(mktemp -d)
trap 'rm -rf "$STUB_BIN"' EXIT
export PATH="$STUB_BIN:$PATH"
# Write the stub
cat > "$STUB_BIN/curl" << 'EOF'
#!/usr/bin/env bash
# Fake curl: always returns a 200 OK with a JSON body
echo '{"status": "ok", "version": "1.2.3"}'
exit 0
EOF
chmod +x "$STUB_BIN/curl"
# Verify the stub is found before the real curl
which curl
curl https://example.com/api/versioncurlを呼び出すスクリプトをテストする
ここまでの内容を組み合わせてみましょう。ヘルスエンドポイントの確認にcurlを呼び出し、サービスが正常でなければエラーで終了するデプロイスクリプトがあるとします。実際のサーバーを使わずに、正常系と異常系の両方をテストしたいとします。
#!/usr/bin/env bash
# Script under test: check_health.sh
# It calls curl and checks the returned JSON
check_health() {
local url="$1"
local response
response=$(curl -sf "$url")
if [[ "$response" == *'"healthy":true'* ]]; then
echo "Service is UP"
return 0
else
echo "Service is DOWN" >&2
return 1
fi
}
# ---- Test harness ----
STUB_BIN=$(mktemp -d)
trap 'rm -rf "$STUB_BIN"' EXIT
export PATH="$STUB_BIN:$PATH"
# Happy path stub
cat > "$STUB_BIN/curl" << 'EOF'
#!/usr/bin/env bash
echo '{"healthy":true}'
EOF
chmod +x "$STUB_BIN/curl"
check_health "http://fake-host/health" && echo "PASS: healthy response"コマンドの失敗をシミュレートする
スタブの最も価値ある用途の一つは、実際のツールでは再現が難しい失敗をシミュレートすることです。たとえば、ネットワークタイムアウト、権限エラー、ディスク容量不足、リモート API が 500 を返す場合などです。
失敗をシミュレートするには、スタブをゼロ以外のコードで終了させるだけです。また、実際のコマンドと同じように stderr に出力することもできるため、スクリプトのエラーハンドリングを完全に検証できます。
#!/usr/bin/env bash
# Test that check_health handles a curl failure gracefully
check_health() {
local url="$1"
local response
# -f makes curl exit non-zero on HTTP error; -s silences progress
if ! response=$(curl -sf "$url" 2>/dev/null); then
echo "ERROR: could not reach $url" >&2
return 1
fi
echo "OK: $response"
}
STUB_BIN=$(mktemp -d)
trap 'rm -rf "$STUB_BIN"' EXIT
export PATH="$STUB_BIN:$PATH"
# Failure stub — simulates a network error (curl exit code 6 = could not resolve host)
cat > "$STUB_BIN/curl" << 'EOF'
#!/usr/bin/env bash
echo 'curl: (6) Could not resolve host: fake-host' >&2
exit 6
EOF
chmod +x "$STUB_BIN/curl"
if ! check_health "http://fake-host/health"; then
echo "PASS: failure path handled correctly"
fi検証のためにスタブ呼び出しを記録する
スクリプトが何を出力したかだけでなく、外部ツールをどのように呼び出したか、つまり渡した引数、呼び出し回数、呼び出し順序などを検証したい場合があります。スパイスタブは、その呼び出しをファイルに記録します。
テスト後、テストハーネスで記録ファイルを読み込み、その内容を検証します。これにより、特別なフレームワークを使わずに引数レベルの検証ができます。
#!/usr/bin/env bash
# Spy stub: record every invocation of 'aws' to a log file
STUB_BIN=$(mktemp -d)
CALL_LOG=$(mktemp)
trap 'rm -rf "$STUB_BIN" "$CALL_LOG"' EXIT
export PATH="$STUB_BIN:$PATH"
export CALL_LOG # make it available inside the stub
cat > "$STUB_BIN/aws" << 'EOF'
#!/usr/bin/env bash
# Append all arguments to the call log
echo "aws $*" >> "$CALL_LOG"
# Return fake S3 output
echo "upload: ./report.pdf to s3://my-bucket/report.pdf"
exit 0
EOF
chmod +x "$STUB_BIN/aws"
# Simulate the script under test calling aws s3 cp
aws s3 cp report.pdf s3://my-bucket/report.pdf
aws s3 cp logs.tar.gz s3://my-bucket/logs.tar.gz
# Verify calls were made with expected arguments
echo "--- Recorded calls ---"
cat "$CALL_LOG"
grep -q 's3://my-bucket/report.pdf' "$CALL_LOG" && echo "PASS: S3 upload verified"複数のコマンドを一度にスタブ化する
実際のスクリプトでは、複数の外部ツールを呼び出すことがよくあります。同じ STUB_BIN ディレクトリに、それらすべてのスタブを配置できます。各スタブファイルは独立しており、それぞれ異なる出力や終了コードを返せます。
スタブは最小限に保ってください。テスト対象のスクリプトが実際に解析するものだけを返します。すべてのフラグをシミュレートしようとせず、スクリプトが使用するサブセットだけを扱ってください。
#!/usr/bin/env bash
# Stub both 'git' and 'docker' for a release script test
STUB_BIN=$(mktemp -d)
trap 'rm -rf "$STUB_BIN"' EXIT
export PATH="$STUB_BIN:$PATH"
# Stub git: pretend we are on tag v2.1.0
cat > "$STUB_BIN/git" << 'EOF'
#!/usr/bin/env bash
case "$*" in
*"describe --tags"*) echo "v2.1.0" ;;
*"rev-parse HEAD"*) echo "abc1234" ;;
*) echo "[git stub] unhandled: $*" >&2 ; exit 1 ;;
esac
EOF
chmod +x "$STUB_BIN/git"
# Stub docker: pretend build and push succeed
cat > "$STUB_BIN/docker" << 'EOF'
#!/usr/bin/env bash
echo "[docker stub] $*"
exit 0
EOF
chmod +x "$STUB_BIN/docker"
# Simulate the release logic
VERSION=$(git describe --tags)
SHA=$(git rev-parse HEAD)
echo "Building image for version=$VERSION sha=$SHA"
docker build -t "myapp:$VERSION" .
docker push "myapp:$VERSION"スタブとして関数を使う(ファイル不要)
単純なケースでは、ファイルをまったく作成する必要はありません。コマンドと同じ名前のシェル関数を定義できます。関数は外部の PATH 検索より先に解決されるため、自動的に優先されます。
これは、source されたスクリプトをユニットテストする最も高速な方法です。ただし、関数スタブが機能するのは同じシェルプロセス内だけです。明示的な bash -c で起動したサブシェルやバックグラウンドプロセスからは見えません。その場合は、ファイルベースの方法を使用してください。
#!/usr/bin/env bash
# Source the script under test (a small helper library)
source_under_test() {
# Inline the logic we want to test
get_instance_id() {
# Would normally call: curl http://169.254.169.254/latest/meta-data/instance-id
curl -sf http://169.254.169.254/latest/meta-data/instance-id
}
}
source_under_test
# Override curl with a shell function stub
curl() {
echo "i-0abc123def456"
return 0
}
# Export is NOT needed — function is visible in same shell
# Run the function under test
result=$(get_instance_id)
[[ "$result" == "i-0abc123def456" ]] && echo "PASS: instance ID returned" || echo "FAIL"関数をサブシェルにエクスポートする
テスト対象のスクリプトがサブシェル(例: bash script.sh やパイプライン)を起動する場合、親シェルで定義したシェル関数スタブは、デフォルトでは継承されません。方法は二つあります。
export -f function_nameを使って関数をエクスポートする。子bashプロセスから利用できるようになります- または、プロセス境界を越えて常に機能する、
STUB_BINディレクトリ内のファイルベースのスタブに切り替える
export -f は便利ですが、bash でのみ機能し、sh などの他のシェルでは機能しません。複数言語を扱う CI 環境では、ファイルベースのスタブを優先してください。
#!/usr/bin/env bash
# Demonstrate export -f for subshell-visible function stubs
# Define the stub in the current shell
curl() {
echo '{"status":"ok"}'
return 0
}
# Export the function so child bash processes inherit it
export -f curl
# Verify the stub works in a subshell
bash -c '
response=$(curl -sf http://api.example.com/status)
echo "Subshell got: $response"
'
# Without export -f, the subshell would call the real curl
# (or fail if curl is not installed)テストフレームワーク(BATS)にスタブを統合する
BATS(Bash Automated Testing System)を使用する場合、スタブのセットアップは setup() フックに、後処理は teardown() に記述します。BATS はテスト間で環境をリセットするため、各テストで新しいスタブディレクトリが用意されます。
$BATS_TEST_TMPDIR などの BATS の変数を使うと、テストごとの一時ディレクトリが自動的に用意されます。コードをすっきりさせるため、mktemp -d の代わりに使用してください。
#!/usr/bin/env bats
# File: test_deploy.bats
# Run with: bats test_deploy.bats
setup() {
# BATS provides a unique tmpdir per test
export STUB_BIN="$BATS_TEST_TMPDIR/stub_bin"
mkdir -p "$STUB_BIN"
export PATH="$STUB_BIN:$PATH"
# Default stub: healthy service
cat > "$STUB_BIN/curl" << 'EOF'
#!/usr/bin/env bash
echo '{"healthy":true}'
EOF
chmod +x "$STUB_BIN/curl"
}
teardown() {
# BATS auto-removes BATS_TEST_TMPDIR, but explicit is safer
rm -rf "$STUB_BIN"
}
@test "deploy succeeds when service is healthy" {
run bash deploy.sh
[ "$status" -eq 0 ]
[[ "$output" == *"Deploy complete"* ]]
}
@test "deploy aborts when service is down" {
# Override the stub for this specific test
echo -e '#!/usr/bin/env bash\nexit 1' > "$STUB_BIN/curl"
chmod +x "$STUB_BIN/curl"
run bash deploy.sh
[ "$status" -ne 0 ]
}理解度チェック: Bash でコマンドをモックする
Bash におけるコマンドのモックとスタブのパターンについて、理解度を確認しましょう。
ある開発者が、実際の AWS CLI の代わりに使うため、aws という名前のシェル関数を定義するテストを書きました。テストをターミナルから直接実行すると正常に動作しますが、CI パイプラインでテスト対象のスクリプトを bash deploy.sh として実行すると、スタブが無視され、実際の aws コマンドが呼び出されます。
正しい修正方法は何でしょうか。
まとめ: コマンドのモックと外部ツールのスタブ化
Bash スクリプトのテスト中に、実際のコマンドを制御可能な偽物に置き換えるための一通りの手法を学びました。
学習した主な手法:
- PATH の先頭への追加 —
mktemp -dでSTUB_BINディレクトリを作成し、そこに実行可能なスタブファイルを書き込み、そのディレクトリをPATHの先頭に追加します - スタブの終了コード — 成功時は
0を返し、ネットワークエラーや権限拒否など特定の失敗をシミュレートする場合はゼロ以外の値を返します - スパイスタブ — スタブ内で引数をログファイルに追記し、スクリプトが外部ツールをどのように呼び出したかを検証します
- 複数のスタブ — 同じ
STUB_BINに複数のスタブファイルを配置し、依存関係にあるエコシステム全体を一度にモックします - 関数スタブ — 同じプロセス内でモックするため、コマンドと同じ名前のシェル関数を定義します。子 bash プロセスから利用するには
export -fを使用します - BATS との統合 —
setup()/teardown()フックと$BATS_TEST_TMPDIRを使い、テストごとにクリーンな分離環境を用意します
テスト結果にかかわらずスタブを確実に削除できるよう、必ず trap '...' EXIT を使用してください。スタブは最小限に保ち、スクリプトが実際に解析するものだけを返します。CI パイプラインでは、ファイルベースのスタブが最も移植性の高い選択肢です。
よくある質問
「コマンドのモックと外部ツールのスタブ化」レッスンは無料ですか?
はい。「コマンドのモックと外部ツールのスタブ化」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Linux Command Line & Bash Scripting Masteryコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Linux Command Line & Bash Scripting Masteryコースには全4レッスンが含まれています。
「コマンドのモックと外部ツールのスタブ化」で何を学びますか?
PATHを上書きして偽のバイナリを定義し、実際のシステムに触れずにスクリプトをテストします。 ブラウザで直接実行するハンズオンコードでLinux Command Line & Bash Scripting Masteryを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
Linux Command Line & Bash Scripting Masteryを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのLinux Command Line & Bash Scripting Masteryは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。
「コマンドのモックと外部ツールのスタブ化」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このLinux Command Line & Bash Scripting Masteryレッスンでコードを書いて実行できますか?
はい。すべてのLinux Command Line & Bash Scripting Masteryレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- Bats-coreによる関数の単体テスト
- コマンドのモックと外部ツールのスタブ化
- フィクスチャ、一時環境、カバレッジ
- CIパイプラインでのシェルテスト実行