0Pricing
Linux Command Line & Bash Scripting Mastery · レッスン

コマンドのモックと外部ツールのスタブ化

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まで到達することはありません。

上書きのパターン:

  1. 一時ディレクトリ(stub bin)を作成します
  2. 実際のコマンドと同じ名前の偽の実行ファイルを書き込みます
  3. そのディレクトリをPATHの先頭に追加します
  4. テスト対象のスクリプトを実行します。実際のバイナリではなく、用意したstubが呼び出されます
  5. テスト後に一時ディレクトリを削除します

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/version

curlを呼び出すスクリプトをテストする

ここまでの内容を組み合わせてみましょう。ヘルスエンドポイントの確認に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フィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. Bats-coreによる関数の単体テスト
  2. コマンドのモックと外部ツールのスタブ化
  3. フィクスチャ、一時環境、カバレッジ
  4. CIパイプラインでのシェルテスト実行
← Linux Command Line & Bash Scripting Masteryに戻る