bats-core の使い方|関数単位テスト

テスト&品質
スポンサーリンク

この記事の狙い

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-supportbats-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 -dteardown
  • 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 コマンドのオプションや実行方法についてはこちらにもまとめています。

参考リンク

スポンサーリンク
Bash玄

はじめまして!Bash玄です。

エンジニアとしてシステム運用に携わる中で、手作業の多さに限界を感じ、Bashスクリプトを活用して業務を効率化したのがきっかけで、この道に入りました。「手作業は負け」「スクリプトはシンプルに」をモットーに、誰でも実践できるBashスクリプトの書き方を発信しています。

このサイトでは、Bashの基礎から実践的なスクリプト作成まで、初心者でもわかりやすく解説しています。少しでも「Bashって便利だな」と思ってもらえたら嬉しいです!

# 好きなこと
- シンプルなコードを書くこと
- コマンドラインを快適にカスタマイズすること
- 自動化で時間を生み出すこと

# このサイトを読んでほしい人
- Bashに興味があるけど、何から始めればいいかわからない人
- 定型業務を自動化したい人
- 効率よくターミナルを使いこなしたい人

Bashの世界に一歩踏み出して、一緒に「Bash道」を極めていきましょう!

Bash玄をフォローする