この記事の狙い
Bash スクリプトの関数レベルを素早く検証するために、bats-core を使った最小セットアップと実践パターンを身につけます。
入出力・終了コード・標準エラーの検証、フィクスチャの扱い、ヘルパ関数の共有、CI 実行までをコピペ可の形でまとめます。
前提と対象
- Bash 4+(macOS は Homebrew などで新しめの bash を推奨)
bats-coreをテストランナーとして使用- 既に「最小テスト入門」で入出力比較・期待 exit の考え方に慣れている方向け
TL;DR(最小実装・コピペ可)
インストール(任意の一つ)
# Homebrew (macOS/Linuxbrew)
brew install bats-core
# npm (node があれば)
npm install -g bats
# git submodule(リポジトリ内に同梱)
git submodule add https://github.com/bats-core/bats-core test/bats-core
# 実行: test/bats-core/bin/bats test
最小テスト
# test/sample.bats
#!/usr/bin/env bats
# 実行対象のパスを準備
setup() {
SUT="${BATS_TEST_DIRNAME}/../scripts/greeter.sh"
}
@test "prints greeting to stdout" {
run "$SUT" --name "bash"
[ "$status" -eq 0 ]
[ "$output" = "hello, bash" ] # stdout を $output で検証
}
@test "prints usage and exits 2 when missing name" {
run "$SUT"
[ "$status" -eq 2 ]
[[ "$error" =~ Usage: ]] # stderr は $error で参照
}
# scripts/greeter.sh(テスト対象:例)
#!/usr/bin/env bash
set -Eeuo pipefail
usage(){ printf 'Usage: %s --name NAME\n' "${0##*/}" >&2; }
name=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) name="${2-}"; shift 2 ;;
-h|--help) usage; exit 2 ;;
*) usage; exit 2 ;;
esac
done
[[ -n "$name" ]] || { usage; exit 2; }
printf 'hello, %s\n' "$name"
実行:
bats test/sample.bats
設計の要点(原則)
- テスト 1 件 = 1 事実(出力・終了コード・副作用など)
- setup/teardown でテスト間の独立性を担保(作業ディレクトリ、一時ファイル)
- stdout / stderr / exit の 3 点セットを明示的に検証
- ヘルパ関数を別ファイル化して
loadで共有(重複を排除)
ステップ実装(分解解説)
段階1:run/出力・終了コードの基本
run <cmd> が失敗してもテスト自体は継続。$status, $output, $lines, $error が利用できます。
@test "exit code and outputs" {
run bash -c 'echo ok; echo err >&2; exit 7'
[ "$status" -eq 7 ]
[ "$output" = "ok" ]
[ "$error" = "err" ]
}
段階2:setup/teardown と一時領域
各テストを隔離して副作用を消すのが鉄則。
setup() {
TMPDIR="$(mktemp -d)"; export TMPDIR
}
teardown() {
rm -rf "${TMPDIR:-}"
}
テスト本体では "$TMPDIR/file.txt" のように常に明示パスで扱います。
段階3:フィクスチャ(入力例・期待出力)
test/fixtures/ 配下に置き、差分を検証。
fixture() { printf '%s' "$BATS_TEST_DIRNAME/fixtures/$1"; }
@test "transforms input into expected output" {
run "${SUT}" <"$(fixture input.txt)"
[ "$status" -eq 0 ]
diff -u "$(fixture expected.txt)" <(printf '%s\n' "$output")
}
段階4:関数の“直接テスト”
Bash 関数を直接呼びたい場合は、関数を別ファイルに分離して source し、run bash -c 経由で実引数を与えると安定します。
# src/lib.sh
to_upper(){ printf '%s' "${1^^}"; }
# test/lib.bats
setup(){ LIB="${BATS_TEST_DIRNAME}/../src/lib.sh"; }
@test "to_upper returns uppercased string" {
run bash -c 'source "$1"; to_upper "abC"' _ "$LIB"
[ "$status" -eq 0 ]
[ "$output" = "ABC" ]
}
直接
source "$LIB"してto_upperを呼ぶ方法でもよいですが、子シェルを介すとset -u/-e等の影響を局所化でき、汚染が少なくなります。
段階5:ヘルパ・共通アサーション
bats-support と bats-assert を入れると可読性が大幅に向上します。
# test/test_helper.bash
load 'test_helper/bats-support/load'
load 'test_helper/bats-assert/load'
# test/with_asserts.bats
setup(){ SUT="${BATS_TEST_DIRNAME}/../scripts/greeter.sh"; }
@test "happy path (assert style)" {
run "$SUT" --name foo
assert_success
assert_output "hello, foo"
}
@test "missing args (assert style)" {
run "$SUT"
assert_failure 2
assert_line --regexp 'Usage:'
}
段階6:環境・依存のモック
環境変数や外部コマンドを差し替え。
@test "uses API_TOKEN env when set" {
run bash -c 'API_TOKEN=xyz source "$1"; print_token' _ "$SUT"
assert_success
assert_output "xyz"
}
@test "mocks external command" {
run bash -c '
PATH="$BATS_TEST_DIRNAME/mocks:$PATH"
export PATH
source "$1"
call_curl
' _ "$SUT"
assert_success
}
test/mocks/curl を置いて、固定の出力を返すダミーに。
失敗しやすい点(アンチパターン)
- テスト間で一時ファイルを共有:並列化や再実行で壊れる →
mktemp -d+teardown - stdout にログを出す:比較が壊れる → ログは stderr(
>&2) - 1 テストで複数事実を検証:失敗原因が不明瞭 → 小さく分解
set -eの影響で途中で落ちる:runを使い、終了コードは$statusで検証
CI 実行(最小ジョブ)
# .github/workflows/bats.yml
name: bats
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install bats + helpers
run: |
sudo apt-get update
sudo apt-get install -y bats
git clone --depth=1 https://github.com/bats-core/bats-support test/test_helper/bats-support
git clone --depth=1 https://github.com/bats-core/bats-assert test/test_helper/bats-assert
- name: Run tests
run: bats -r test
セキュリティと安全設計
- テストが外部ネットワークに出ない設計(モック/フィクスチャで代替)
- パスの結合はクォート徹底、一時領域は
mktemp -dのみ使用 - 環境変数の注入はテスト側で明示(グローバルの引きずり込みを避ける)
パフォーマンスの勘所(短く)
- 小さなユニットテストは 秒未満 で走るのが目安。重い I/O はモックで差し替え
- フィクスチャは最小の代表データだけにし、差分が必要なときだけ増やす
参考実装(拡張版・コピペ可)
テンプレート一式。これを土台に増やしていけます。
repo/
├─ scripts/
│ └─ greeter.sh
├─ src/
│ └─ lib.sh
├─ test/
│ ├─ sample.bats
│ ├─ lib.bats
│ ├─ with_asserts.bats
│ ├─ test_helper/
│ │ ├─ bats-support/ (git clone)
│ │ └─ bats-assert/ (git clone)
│ ├─ fixtures/
│ │ ├─ input.txt
│ │ └─ expected.txt
│ └─ mocks/
│ └─ curl
└─ .github/workflows/bats.yml
よくある質問(Q&A)
Q. bats と生 Bash の run.sh、どちらを先に導入?
A. まずは生 Bash の最小テスト(1 ファイル)。習慣化できたら bats に寄せると、テストの見通しが上がります。
Q. スナップショット的に出力全部を固定して良い?
A. まずは代表行だけを固定・比較。出力全文の固定は更新コストが高いので、必要な場面だけに絞ります。
Q. 並列実行は?
A. bats -j <N> で並列可。ただし一時領域の衝突に注意し、毎テスト独立を満たす構成にします。
bats コマンドのオプションや実行方法についてはこちらにもまとめています。
