フィクスチャ、一時環境、カバレッジ
分離されたテストフィクスチャを構築し、テストが実際に実行しているスクリプトの分岐を測定します。
「フィクスチャ、一時環境、カバレッジ」はCoddyKit上の無料DevOps Bootcampレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはDevOps Bootcamp学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 DevOps Bootcampコースには全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 などの外部ツールを呼び出します。テストでは実際のサービスに接続したくないため、それらのツールを偽の実行ファイルに置き換えます。
手順は簡単です。
- フィクスチャ内に一時的な
bin/ディレクトリを作成します - 実際のツールと同じ名前の小さなシェルスクリプトをそこに書き込みます
- テスト対象のスクリプトを呼び出す前に、そのディレクトリを
$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チューター)、DevOps Bootcampコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 DevOps Bootcampコースには全4レッスンが含まれています。
「フィクスチャ、一時環境、カバレッジ」で何を学びますか?
分離されたテストフィクスチャを構築し、テストが実際に実行しているスクリプトの分岐を測定します。 ブラウザで直接実行するハンズオンコードでDevOps Bootcampを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
DevOps Bootcampを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのDevOps Bootcampは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。
「フィクスチャ、一時環境、カバレッジ」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このDevOps Bootcampレッスンでコードを書いて実行できますか?
はい。すべてのDevOps Bootcampレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- Bats-coreによる関数の単体テスト
- コマンドのモックと外部ツールのスタブ化
- フィクスチャ、一時環境、カバレッジ
- CIパイプラインでのシェルテスト実行