2026年5月30日
2026年5月30日
シェルスクリプトのテストをbatsで書く方法:導入からコマンドの差し替え・CIまで
シェルスクリプトのテストは、bats(Bash Automated Testing System、現在の本流は bats-core)を使うと、Bash の文法のまま「この入力ならこの出力・終了コードになる」と書けます。この記事では、bats の導入、テストの基本形、外部コマンド(wp など)を偽物に差し替えるテスト、CI での実行までを、WordPress のバックアップスクリプトを題材に順に説明します。
batsとは何か、何をテストするのか
bats は .bats という拡張子のファイルに @test "名前" { ... } というブロックを並べ、各ブロックを独立したテストケースとして実行するツールです。ブロック内のコマンドが失敗した時点でそのテストは失敗扱いになり、最後に成功・失敗の件数が表示されます。
シェルスクリプトでテストする価値が高いのは、次のような部分です。
- 引数の検証(必須引数が空のときにエラーで止まるか)
- ファイル名や日付の組み立てなど、純粋な文字列処理
- 外部コマンドに正しい引数を渡しているか(本物の
wp db exportを動かさずに確認する) - 異常時の終了コード(cron や CI から呼ばれたときに失敗を検知できるか)
逆に、本番サーバーへの接続や実データベースの書き換えをテストで直接行うのは避けます。テストは何度でも安全に流せることが前提なので、外部への副作用は後述の「差し替え」で閉じ込めます。
導入:プロジェクトに同梱する方法
bats はパッケージマネージャー(Homebrew の bats-core、npm の bats、各ディストリビューションの bats パッケージ)でも入りますが、ディストリビューションのパッケージは版が古いことがあります(2026年10月現在も、配布元によって収録バージョンに差があります)。チーム内や CI で同じ版を使うには、git のサブモジュールとしてプロジェクトに含める方法が扱いやすいです。
git submodule add https://github.com/bats-core/bats-core.git test/bats
git submodule add https://github.com/bats-core/bats-support.git test/test_helper/bats-support
git submodule add https://github.com/bats-core/bats-assert.git test/test_helper/bats-assert
./test/bats/bin/bats --version
bats-support と bats-assert は、assert_success や assert_output といった読みやすい検証用関数を提供する補助ライブラリです。なくても [ "$status" -eq 0 ] のように書けますが、失敗時に「期待値と実際の値」を並べて表示してくれるので、原因の特定が速くなります。
ディレクトリ構成は次のようにしておくと、以降の例がそのまま使えます。
project/
├── backup.sh
└── test/
├── backup.bats
├── bats/
└── test_helper/
├── bats-support/
└── bats-assert/
テストしやすいスクリプトの形にする
テスト対象のスクリプトは、処理を関数に分け、直接実行されたときだけ main を呼ぶ形にします。こうすると、テストからは source で関数だけを読み込めます。
#!/usr/bin/env bash
# backup.sh : WordPress の DB をファイルに書き出す
backup_filename() {
local site="$1" date="$2"
if [[ -z "$site" ]]; then
echo "site name is required" >&2
return 2
fi
printf '%s-%s.sql\n' "$site" "$date"
}
main() {
local site="${1:-}"
local dest="${BACKUP_DIR:-/var/backups/wp}"
local file
file="$(backup_filename "$site" "$(date +%Y%m%d)")" || return $?
mkdir -p "$dest"
wp db export "$dest/$file" --quiet
echo "saved: $dest/$file"
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
set -euo pipefail
main "$@"
fi
set -euo pipefail をファイル先頭ではなく実行時の分岐の中に置いているのがポイントです。先頭に書くと、テストで source した瞬間に bats 側のシェルにも -u などが効いてしまい、bats の内部処理と干渉することがあります。
基本のテストを書く
#!/usr/bin/env bats
# test/backup.bats
bats_require_minimum_version 1.5.0
setup() {
load 'test_helper/bats-support/load'
load 'test_helper/bats-assert/load'
source "$BATS_TEST_DIRNAME/../backup.sh"
}
@test "ファイル名はサイト名と日付から作る" {
run backup_filename example 20261018
assert_success
assert_output 'example-20261018.sql'
}
@test "サイト名が空なら終了コード2で止まる" {
run -2 backup_filename "" 20261018
assert_output --partial 'required'
}
setup は各テストの前に毎回実行されます。run はコマンドを実行し、終了コードを $status、出力を $output(行ごとの配列は $lines)に入れます。run 自体はコマンドが失敗してもテストを失敗させないため、その後に assert_success などで結果を検証する、という流れになります。標準エラー出力も既定では $output に混ざるので、エラーメッセージの確認もこの形で書けます。
run -2 のように期待する終了コードを run に渡す書き方は、比較的新しい bats-core(1.5.0 以降)の機能です。ファイル先頭の bats_require_minimum_version 1.5.0 は「この版以上を前提にする」という宣言で、これがないと警告が出る版があります。
実行は次のとおりです。
./test/bats/bin/bats test/
外部コマンドを偽物に差し替える
main は wp db export を呼ぶので、そのままテストすると本物のデータベースに触れてしまいます。そこで、テスト用の一時ディレクトリに同名の偽コマンド(スタブ)を置き、PATH の先頭に追加します。
@test "main は wp db export に保存先を渡す" {
mkdir -p "$BATS_TEST_TMPDIR/bin"
export WP_CALLS="$BATS_TEST_TMPDIR/wp_calls.log"
cat > "$BATS_TEST_TMPDIR/bin/wp" <<'EOF'
#!/usr/bin/env bash
echo "$*" >> "$WP_CALLS"
EOF
chmod +x "$BATS_TEST_TMPDIR/bin/wp"
PATH="$BATS_TEST_TMPDIR/bin:$PATH"
export BACKUP_DIR="$BATS_TEST_TMPDIR/out"
run main example
assert_success
assert_output --partial "saved: $BATS_TEST_TMPDIR/out/example-"
run cat "$WP_CALLS"
assert_output --partial "db export $BATS_TEST_TMPDIR/out/example-"
}
BATS_TEST_TMPDIR はテストごとに用意される一時ディレクトリで、テスト終了後に片付けられます。スタブは受け取った引数をファイルに記録するだけなので、「どんな引数で呼ばれたか」を後から検証できます。同じ考え方で curl・mysqldump・rsync なども差し替えられます。
つまずきやすい点
bats 特有の落とし穴を知っておくと、「テストが通っているのに実は何も検証していない」状態を防げます。
run cmd | grep xと書くと、パイプはrunの出力(空)に対してかかります。パイプを含めて検証したいときはrun bash -c 'cmd | grep x'のように1つのコマンドとして渡します。runはサブシェルで実行されるため、中で変更した変数はテスト本体に反映されません。変数の値を確かめたいときはrunを使わずに関数を直接呼びます。! cmdのような否定は、テストの失敗として扱われないことがあります。「失敗すること」を確かめたいときはrun !か、runのあとにassert_failureを使います。- macOS 標準の古い Bash(3.2系)では、途中の行の
[[ ]]が偽でもテストが失敗にならない場合があると bats のドキュメントで注意されています。CI と手元で Bash の版をそろえるか、assert_*系の関数を使うと安全です。
失敗したときは、bats --verbose-run や --print-output-on-failure などのオプションで詳細を出せます(オプション名は版によって異なるため、bats --help で確認してください)。
CIで自動実行する
GitHub Actions なら、サブモジュールごとチェックアウトして実行するだけで動きます。
name: shell-test
on: [push, pull_request]
jobs:
bats:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- run: ./test/bats/bin/bats --formatter tap test/
アクションのバージョンは時期によって更新されるため、利用時点の推奨版に合わせてください。bats には JUnit 形式のレポートを出す機能もあり、CI のテスト結果画面に表示させたい場合に使えます。あわせて ShellCheck による静的解析を同じジョブで流すと、テストでは拾いにくい引用符の付け忘れなども検出できます。
まとめ
- bats は
@testブロックとrunで、出力と終了コードを Bash のまま検証できる。 - bats-core 本体と bats-support・bats-assert はサブモジュールで同梱すると版をそろえやすい。
- スクリプトは関数に分け、直接実行時だけ
mainを呼ぶ形にするとsourceしてテストできる。 wpなどの外部コマンドはBATS_TEST_TMPDIRに置いたスタブで差し替え、本番環境に触れない。- パイプ・サブシェル・否定の扱いなど、bats 特有の落とし穴を押さえてから CI に組み込む。