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

フィクスチャ、一時環境、カバレッジ

分離されたテストフィクスチャを構築し、テストが実際に実行しているスクリプトの分岐を測定します。

「フィクスチャ、一時環境、カバレッジ」はCoddyKit上の無料Linux Command Line & Bash Scripting Masteryレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはLinux Command Line & Bash Scripting Mastery学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Linux Command Line & Bash Scripting Masteryコースには全4レッスンが含まれています。

フィクスチャと分離が重要な理由

Bash スクリプトをテストする際の最大のリスクは副作用です。テストによって、実際のファイル、実際のデータベース、または実際のシステム状態が誤って変更される可能性があります。自分のマシンでは成功する一方で本番データを破壊するテストは、テストがまったくないよりも悪いものです。

解決策はテストフィクスチャです。これは、実際の環境に触れることなく、現実の条件を再現する制御可能で破棄可能な環境です。優れたフィクスチャには、次の利点があります。

  • 再現性 — テストを実行するたびに同じ結果になります
  • 分離 — テスト同士やホスト環境と干渉しません
  • 安全性 — 破壊的な操作の対象が使い捨てデータだけになります
  • 速度 — 絶対に必要でない限り、ネットワーク呼び出しや負荷の高い I/O を行いません

Bash のテストでは、フィクスチャは通常、既知のファイルを配置した一時ディレクトリ、$PATH の前方に配置したモック実行ファイル、テストプロセスに限定した環境変数で構成されます。

一時ディレクトリを作成して後片付けする

テストごとの一時ディレクトリには、通常 mktemp -d を使用します。このコマンドは /tmp に一意のディレクトリを作成し、そのパスを出力します。パスを保存し、シェルの終了時に、失敗した場合も含めて自動的に削除する trap を登録します。

ファイルシステムに触れるすべてのテストファイルに、次の 2 行の定型コードを記述してください。

#!/usr/bin/env bash
set -euo pipefail

# Create an isolated temp directory
TMPDIR=$(mktemp -d)
# Always clean up, even if the script exits early or errors out
trap 'rm -rf "$TMPDIR"' EXIT

echo "Working in: $TMPDIR"

# Simulate creating fixture files
mkdir -p "$TMPDIR/project/{src,tests,logs}"
echo 'version=1.2.3' > "$TMPDIR/project/.env"
echo 'Hello fixture' > "$TMPDIR/project/src/main.sh"

ls -R "$TMPDIR/project"
echo 'Temp dir will be removed automatically on exit'

フィクスチャのディレクトリツリーを構成する

適切に構成されたフィクスチャは、テスト対象のスクリプトが実際に想定するディレクトリ構成を再現します。これは小さな偽のプロジェクトルートと考えるとよいでしょう。フィクスチャのセットアップ関数で各テストの前にこの構成を作成し、後処理で削除します。

主な実践方法:

  • テストフレームワークが各テストの前に呼び出す setup() 関数を使用します
  • 失敗した場合も含め、各テストの後に実行される teardown() または cleanup() を使用します
  • フィクスチャファイルは最小限にし、スクリプトが実際に読み取るものだけを用意します
  • 失敗の原因を簡単に特定できるよう、フィクスチャファイルには説明的な名前を付けます
#!/usr/bin/env bash
# fixture_helpers.bash — source this from your test files

FIXTURE_ROOT=''

setup_fixture() {
  FIXTURE_ROOT=$(mktemp -d)
  # Build the directory tree the deploy script expects
  mkdir -p "$FIXTURE_ROOT"/{dist,config,logs}
  echo '{"version":"2.0"}' > "$FIXTURE_ROOT/config/app.json"
  echo 'console.log("app")' > "$FIXTURE_ROOT/dist/index.js"
  touch "$FIXTURE_ROOT/logs/.gitkeep"
  export FIXTURE_ROOT
  echo "[setup] Fixture ready at $FIXTURE_ROOT"
}

teardown_fixture() {
  if [[ -n "$FIXTURE_ROOT" && -d "$FIXTURE_ROOT" ]]; then
    rm -rf "$FIXTURE_ROOT"
    echo '[teardown] Fixture removed'
  fi
}

# Self-test
setup_fixture
ls "$FIXTURE_ROOT"
teardown_fixture

偽の PATH で実行ファイルをモックする

多くの Bash スクリプトは、curl、aws、docker、git などの外部ツールを呼び出します。テストでは実際のサービスに接続したくないため、それらのツールを偽の実行ファイルに置き換えます。

手順は簡単です。

  1. フィクスチャ内に一時的な bin/ ディレクトリを作成します
  2. 実際のツールと同じ名前の小さなシェルスクリプトをそこに書き込みます
  3. テスト対象のスクリプトを呼び出す前に、そのディレクトリを $PATH の先頭に追加します

$PATH は左から右に検索されるため、偽の実行ファイルが優先されます。実際のバイナリが呼び出されることはありません。

#!/usr/bin/env bash
set -euo pipefail

FIXTURE=$(mktemp -d)
trap 'rm -rf "$FIXTURE"' EXIT

# Create a fake 'curl' that records calls and returns canned data
FAKE_BIN="$FIXTURE/bin"
mkdir -p "$FAKE_BIN"

cat > "$FAKE_BIN/curl" << 'SCRIPT'
#!/usr/bin/env bash
# Log every argument for later inspection
echo "curl $*" >> "$FIXTURE_BIN_LOG"
# Return a canned HTTP 200 response body
echo '{"status":"ok","id":42}'
SCRIPT
chmod +x "$FAKE_BIN/curl"

# Export the log path so the fake can find it
export FIXTURE_BIN_LOG="$FIXTURE/curl_calls.log"

# Prepend fake bin directory to PATH
export PATH="$FAKE_BIN:$PATH"

# Now any call to 'curl' hits our fake
curl -s https://api.example.com/health
curl -X POST https://api.example.com/deploy

echo '--- Recorded curl calls ---'
cat "$FIXTURE_BIN_LOG"

コマンド出力を取得して検証する

スクリプトの動作を検証できなければ、フィクスチャは役に立ちません。標準的なパターンは次のとおりです。

  • $() またはプロセス置換を使って、stdout/stderr を変数に取得します
  • 偽のバイナリが書き込んだログファイルを調べます
  • $? または条件分岐を使って、終了コードを明示的に確認します
  • 特定のファイルが作成、変更されたか、または変更されずに残っているかを確認します

小さく焦点を絞ったアサーションヘルパーを書くと、テストが読みやすくなり、問題が発生したときに正確な失敗メッセージを表示できます。

#!/usr/bin/env bash
set -euo pipefail

# Minimal assertion helpers
assert_eq() {
  local desc="$1" expected="$2" actual="$3"
  if [[ "$expected" == "$actual" ]]; then
    echo "PASS: $desc"
  else
    echo "FAIL: $desc"
    echo "  expected: $expected"
    echo "  actual:   $actual"
    return 1
  fi
}

assert_file_exists() {
  local desc="$1" file="$2"
  if [[ -f "$file" ]]; then
    echo "PASS: $desc"
  else
    echo "FAIL: $desc — file not found: $file"
    return 1
  fi
}

assert_contains() {
  local desc="$1" needle="$2" haystack="$3"
  if [[ "$haystack" == *"$needle"* ]]; then
    echo "PASS: $desc"
  else
    echo "FAIL: $desc — '$needle' not found in output"
    return 1
  fi
}

# Demo usage
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
echo 'hello world' > "$TMPDIR/greeting.txt"
OUT=$(cat "$TMPDIR/greeting.txt")
assert_eq   'file content matches'     'hello world' "$OUT"
assert_file_exists 'greeting file created' "$TMPDIR/greeting.txt"
assert_contains    'output has hello'     'hello'       "$OUT"

テストにおける環境変数のスコープ

スクリプトは、$HOME、$CONFIG_PATH、$DATABASE_URL などの環境変数を読み取ることがよくあります。テストでは、実際の環境を汚染せずにこれらを上書きする必要があります。

最も安全な方法は、明示的に設定した変数だけを持つサブシェルでテスト対象のスクリプトを実行することです。env コマンドを使うと、環境をいったん空にして、必要なものだけを追加できます。

  • env -i VAR=val ./script.sh — 完全にクリーンな環境
  • (export VAR=val; ./script.sh) — 親の環境に加えて、指定した上書きを継承するサブシェル

サブシェルを使うと、スクリプトが $IFS、$PWD、その他のグローバル状態を変更しても、その変更がテストランナーに戻ることはありません。

#!/usr/bin/env bash
set -euo pipefail

FIXTURE=$(mktemp -d)
trap 'rm -rf "$FIXTURE"' EXIT

# Write a tiny script under test that reads env vars
cat > "$FIXTURE/deploy.sh" << 'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
ENV_NAME=${DEPLOY_ENV:-unknown}
CFG=${CONFIG_DIR:-/etc/app}
echo "Deploying to $ENV_NAME using config from $CFG"
SCRIPT
chmod +x "$FIXTURE/deploy.sh"

echo '--- Test 1: staging env ---'
# Run in a clean subshell with explicit vars
(
  export DEPLOY_ENV=staging
  export CONFIG_DIR="$FIXTURE/config"
  "$FIXTURE/deploy.sh"
)

echo '--- Test 2: production env ---'
(
  export DEPLOY_ENV=production
  export CONFIG_DIR=/etc/prod-config
  "$FIXTURE/deploy.sh"
)

echo '--- Test 3: defaults ---'
# No env vars set — script should use its own defaults
env -i PATH="$PATH" "$FIXTURE/deploy.sh"

Bash のカバレッジのための kcov 入門

カバレッジは、「テストでスクリプトのどの行(および分岐)が実際に実行されたか」という問いに答えるものです。カバレッジの数値が高くても正しさが保証されるわけではありませんが、低い数値からは、バグが潜んでいる可能性の高い未テストの経路がわかります。

Bash のカバレッジ測定に使う主なツールは kcov です。PTRACE(Linux)または dtrace(macOS)を使って OS レベルでスクリプトを計測するため、ソースコードを変更する必要はありません。未カバーの行を赤、カバー済みの行を緑で示す HTML レポートを生成します。

基本的な使い方:

  • kcov --include-path=./src coverage-out/ ./src/myscript.sh
  • ブラウザーで coverage-out/index.html を開いて結果を確認します
  • CI では、機械的に読み取れる割合を得るために coverage-out/myscript.sh/coverage.json を解析します

注: kcov は別途インストールする必要があります(macOS では brew install kcov、Ubuntu 20.04 以降では apt install kcov)。

実際のスクリプトに対して kcov を実行する

ここでは、デプロイ可能なスクリプト、それを実行するテスト、カバレッジを測定する kcov の呼び出しを一通り示します。後で複数のテスト実行結果をマージできるよう、出力ディレクトリをテストごとに分けている点に注目してください。

#!/usr/bin/env bash
# This demo shows the *structure* of a kcov workflow.
# It will not run kcov itself (not guaranteed to be installed),
# but the script under test and test runner are fully runnable.
set -euo pipefail

FIXTURE=$(mktemp -d)
trap 'rm -rf "$FIXTURE"' EXIT

# 1. Script under test
cat > "$FIXTURE/process.sh" << 'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
INPUT="$1"
if [[ ! -f "$INPUT" ]]; then
  echo "ERROR: file not found" >&2
  exit 1
fi
LINE_COUNT=$(wc -l < "$INPUT")
if (( LINE_COUNT == 0 )); then
  echo "WARNING: file is empty"
else
  echo "Processed $LINE_COUNT lines"
fi
SCRIPT
chmod +x "$FIXTURE/process.sh"

# 2. Happy path test (exercises line 6 and 11)
echo -e 'one\ntwo\nthree' > "$FIXTURE/data.txt"
OUT=$("$FIXTURE/process.sh" "$FIXTURE/data.txt")
echo "Happy path: $OUT"

# 3. Empty file test (exercises line 9)
touch "$FIXTURE/empty.txt"
OUT=$("$FIXTURE/process.sh" "$FIXTURE/empty.txt")
echo "Empty file: $OUT"

# 4. Missing file test (exercises line 5-6)
if ! OUT=$("$FIXTURE/process.sh" "$FIXTURE/missing.txt" 2>&1); then
  echo "Missing file (expected error): $OUT"
fi

# To measure coverage, wrap each call with kcov:
# kcov --include-path="$FIXTURE" "$FIXTURE/cov/happy" "$FIXTURE/process.sh" ...
# kcov --merge "$FIXTURE/cov/all" "$FIXTURE/cov/happy" "$FIXTURE/cov/empty"

複数のテスト実行結果からカバレッジをマージする

一つのテストで、すべての分岐をカバーできることはほとんどありません。そのため、複数のテストをそれぞれ専用のカバレッジ出力ディレクトリで実行し、それらをマージします。kcov の --merge フラグを使うと、複数回の実行結果を一つの統合レポートにまとめられます。

CI パイプラインでの一般的なパターン:

  • テスト A を実行 → cov/test_a/ に出力
  • テスト B を実行 → cov/test_b/ に出力
  • マージ → kcov --merge cov/all/ cov/test_a/ cov/test_b/
  • 最終的な割合を得るために cov/all/<script>/coverage.json を解析

最低しきい値を設定し、カバレッジがそれを下回った場合に CI ビルドを失敗させることもできます。

#!/usr/bin/env bash
# extract_coverage.sh — parse kcov JSON and fail below threshold
set -euo pipefail

MIN_COVERAGE=80   # percent
COV_JSON="${1:-coverage-out/myscript.sh/coverage.json}"

if [[ ! -f "$COV_JSON" ]]; then
  echo "ERROR: coverage JSON not found at $COV_JSON" >&2
  exit 1
fi

# kcov JSON contains a key like: "percent_covered": "87.50"
PERCENT=$(grep -oP '"percent_covered":\s*"\K[0-9.]+' "$COV_JSON")
PERCENT_INT=${PERCENT%%.*}   # truncate decimal

echo "Coverage: ${PERCENT}%  (minimum: ${MIN_COVERAGE}%)"

if (( PERCENT_INT < MIN_COVERAGE )); then
  echo "FAIL: coverage ${PERCENT}% is below threshold ${MIN_COVERAGE}%" >&2
  exit 1
fi

echo "PASS: coverage threshold met"

ブランチカバレッジとラインカバレッジの違い

主に使用されるカバレッジ指標は次の二つです。

  • ラインカバレッジ — その行が一度でも実行されたか。簡単に見かけ上の数値を上げられます。一つのテストで多くの行を実行しながら、重要な条件分岐を見落とす可能性があるためです。
  • ブランチカバレッジ — すべての if、case、&&/|| における各分岐が実行されたか。より信頼性の高い指標です。すべての判定について、真の場合と偽の場合の両方をテストする必要があります。

kcov はこの両方をレポートします。重要な点は、ラインカバレッジが 100% でも、ブランチカバレッジが 100% になるとは限らないことです。次のスクリプトを考えてみましょう。空でないファイルを使った一つのテストですべての行をカバーできますが、空ファイルの分岐(下記の 9 行目)は一度も実行されません。

#!/usr/bin/env bash
# Illustrates line vs branch coverage gap
set -euo pipefail

check_file() {
  local f="$1"
  if [[ -f "$f" ]]; then        # branch A (true) OR branch B (false)
    local lines
    lines=$(wc -l < "$f")
    if (( lines > 0 )); then    # branch C (true) OR branch D (false)
      echo "File has $lines lines"
    else
      echo "File is empty"     # branch D — unreached if only tested with non-empty file
    fi
  else
    echo "File missing"        # branch B — unreached if only tested with existing file
  fi
}

# Only one test: covers lines 5-10 (4 of 6 branches)
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
echo 'data' > "$TMPDIR/sample.txt"
check_file "$TMPDIR/sample.txt"

# To reach 100% branch coverage you also need:
# check_file "/nonexistent/path"
# check_file "$TMPDIR/empty.txt" (after: touch "$TMPDIR/empty.txt")

CIでフィクスチャとカバレッジを統合する

すべてを一つにまとめると、Bashプロジェクト向けの堅牢なCIパイプラインは、フィクスチャのセットアップ、kcovによるテスト実行、マージ、しきい値チェックを1つのスクリプトに統合します。このスクリプトがCIのエントリポイントとなり、1つのコマンドですべてを実行できます。

CIテストランナーの設計原則:

  • 各テストケースでsetup_fixtureを呼び出し、trapを使ってteardown_fixtureを登録します
  • テスト対象のスクリプトは、偽の$PATHとスコープを限定した環境変数を使って呼び出します
  • 各呼び出しをkcovでラップし、番号付きのサブディレクトリに書き込みます
  • すべてのテストの後で、kcovでマージし、しきい値スクリプトでビルドの可否を判定します
  • いずれかのテストまたはカバレッジチェックが失敗した場合、CIランナーは0以外の終了コードで終了します
#!/usr/bin/env bash
# ci_runner.sh — full fixture + coverage pipeline entry point
set -euo pipefail

SRC="./src/deploy.sh"
COV_ROOT="$(mktemp -d)/coverage"
trap 'rm -rf "$COV_ROOT"' EXIT
mkdir -p "$COV_ROOT"

PASS=0
FAIL=0
RUN_NUM=0

run_test() {
  local name="$1" test_fn="$2"
  local fixture
  fixture=$(mktemp -d)
  local cov_out="$COV_ROOT/run_$((++RUN_NUM))"

  if (
    trap 'rm -rf "$fixture"' EXIT
    export FIXTURE="$fixture"
    # Fake bin directory shadowing real tools
    mkdir -p "$fixture/bin"
    export PATH="$fixture/bin:$PATH"
    "$test_fn" "$fixture"
  ); then
    echo "PASS: $name"
    (( PASS++ )) || true
  else
    echo "FAIL: $name"
    (( FAIL++ )) || true
  fi
}

# Example test function
test_happy_path() {
  local fx="$1"
  mkdir -p "$fx/dist"
  echo 'app.js' > "$fx/dist/index.js"
  # Would normally run: kcov "$cov_out" "$SRC" --env=staging "$fx"
  echo "[test] happy path executed in $fx"
}

run_test 'happy_path' test_happy_path

echo "Results: $PASS passed, $FAIL failed"
(( FAIL == 0 ))

理解度チェック:偽のPATHの手法

Bashのテストフィクスチャで使われる、偽のPATHの手法について理解度を確認しましょう。

まとめ:フィクスチャ、一時環境、カバレッジ

このレッスンでは、信頼性が高く分離されたBashテストに必要なツールを一通り学びました。

  • 一時ディレクトリ — mktemp -dとtrap ... EXITを組み合わせることで、テストがどのように終了しても自動的にクリーンアップできます。
  • フィクスチャの構成 — setup_fixtureとteardown_fixtureのペアで、実際のスクリプト入力を再現した最小限のディレクトリツリーを作成・削除します。
  • 偽のPATH — $FIXTURE/bin/にスタブ実行ファイルを配置し、それを$PATHの先頭に追加すると、システムのバイナリに触れることなく、curl、aws、dockerなどの外部ツールへの呼び出しを横取りできます。
  • 環境変数のスコープ — テスト対象のスクリプトをサブシェル(()またはenv -i)で実行し、変更した変数がテストランナーに漏れないようにします。
  • アサーション — 小さなヘルパー関数(assert_eq、assert_file_exists、assert_contains)によって、合否を明確に出力し、意味のあるエラーメッセージを表示できます。
  • kcovによるカバレッジ — ソースコードを変更せずにスクリプトの実行をラップし、行カバレッジとブランチカバレッジのHTMLおよびJSONレポートを生成します。
  • マージとしきい値 — --mergeで複数のkcov実行結果を結合し、JSONを解析して、カバレッジが最低基準を下回った場合にCIを失敗させます。
  • ブランチカバレッジと行カバレッジ — 常にブランチカバレッジを目標にしてください。行カバレッジだけでは、条件分岐の経路全体を見落とし、誤った安心感を招くことがあります。

これらの手法を使えば、Bashのテストもコンパイル言語のテストと同じように厳密なものになります。

よくある質問

「フィクスチャ、一時環境、カバレッジ」レッスンは無料ですか?

はい。「フィクスチャ、一時環境、カバレッジ」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Linux Command Line & Bash Scripting Masteryコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Linux Command Line & Bash Scripting Masteryコースには全4レッスンが含まれています。

「フィクスチャ、一時環境、カバレッジ」で何を学びますか?

分離されたテストフィクスチャを構築し、テストが実際に実行しているスクリプトの分岐を測定します。 ブラウザで直接実行するハンズオンコードでLinux Command Line & Bash Scripting Masteryを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Linux Command Line & Bash Scripting Masteryを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのLinux Command Line & Bash Scripting Masteryは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/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に戻る