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

Bats-coreによる関数の単体テスト

テストファイル、アサーション、セットアップとティアダウンを構成し、個々のBash関数を検証します。

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

Bats-coreとは何か、なぜ使うのか

Bats-core(Bash Automated Testing System)は、Bashのデファクトスタンダードなユニットテストフレームワークです。シェル関数やスクリプトに対して、構造化された再現性のあるテストを記述できます。これは、JavaでJUnit、Pythonでpytestを使うのと同じようなものです。

  • 各テストは、人が読める説明を付けた@testブロックとして記述します。
  • ブロック内のすべてのコマンドが終了コード0を返すと、テストは成功します。
  • ゼロ以外の終了コードが返るか、アサーションに失敗すると、テストは最初の時点で失敗します。
  • 出力はTAP互換なので、CIシステム(GitHub Actions、Jenkins、GitLab CI)は標準で認識できます。

パッケージマネージャーを使ってインストールするか、リポジトリをcloneします。

# Install via git (recommended — always latest)
git clone https://github.com/bats-core/bats-core.git
cd bats-core && sudo ./install.sh /usr/local

# Or on macOS with Homebrew
brew install bats-core

# Verify installation
bats --version
# bats 1.x.y

最初のBatsテストファイル

Batsのテストファイルの拡張子は.batsで、専用のshebangから始まります。基本となる構成要素は、説明文字列に続けてコマンドブロックを記述する@testディレクティブです。

  • shebangの#!/usr/bin/env batsは、ファイルの実行方法をシェルに伝えます。
  • 各@testブロックは、独立したテストケースです。
  • 1つのファイルをbats my_tests.batsで実行したり、ディレクトリ全体をbats test/で実行したりできます。

以下は、Batsテストファイルの最小構成です。

#!/usr/bin/env bats
# File: test/hello.bats

@test "echo outputs the expected string" {
  result=$(echo "hello world")
  [ "$result" = "hello world" ]
}

@test "false command causes test to fail" {
  # Uncommenting the next line would make this test fail:
  # false
  true
}

'load'でテスト対象の関数を読み込む

実際のプロジェクトでは、Bash関数はテストファイル内ではなく、ライブラリファイルに記述します。Batsには、テストファイルのディレクトリを基準に外部ファイルをsourceするloadヘルパーがあります。

  • load '../lib/math.sh'は、各テストの実行前にファイルをsourceします。
  • 読み込み後は、そのファイルで定義されたすべての関数をテストブロックから利用できます。
  • 役割を明確に分離するため、ライブラリ関数はlib/ディレクトリに、テストはtest/ディレクトリに置きます。

プロジェクトの構成例と、それに対応するテストを見てみましょう。

# Project layout:
# lib/math.sh       <- functions to test
# test/math.bats    <- test file

# lib/math.sh
add() {
  echo $(( $1 + $2 ))
}

divide() {
  if [ "$2" -eq 0 ]; then
    echo "Error: division by zero" >&2
    return 1
  fi
  echo $(( $1 / $2 ))
}

# test/math.bats
#!/usr/bin/env bats

load '../lib/math.sh'

@test "add returns correct sum" {
  result=$(add 3 4)
  [ "$result" = "7" ]
}

基本アサーション:run、$status、$output

runコマンドは、Batsテストの中心となる機能です。コマンドを直接実行する代わりにrunでラップすると、テストをただちに失敗させずに終了コードと出力を取得できます。

  • $status — 直前のrunコマンドの終了コードを保持します。
  • $output — 直前のrunコマンドの標準出力全体を保持します。
  • $lines — 出力の各行を要素として持つ配列です(${lines[0]}、${lines[1]}など)。

これにより、成功するケースと失敗するケースの両方をアサートできます。

#!/usr/bin/env bats

load '../lib/math.sh'

@test "divide 10 by 2 returns 5" {
  run divide 10 2
  [ "$status" -eq 0 ]
  [ "$output" = "5" ]
}

@test "divide by zero returns exit code 1" {
  run divide 10 0
  [ "$status" -eq 1 ]
}

@test "divide by zero prints error message" {
  run divide 10 0
  # $output captures stderr too when redirected inside the function
  [[ "$output" == *"division by zero"* ]]
}

bats-assertで表現力の高いアサーションを使う

組み込みの[ ]アサーションは動作しますが、失敗時のメッセージが分かりにくいという問題があります。bats-assertヘルパーライブラリを使うと、何が問題だったのかを正確に表示する、表現力の高いアサーション関数を利用できます。

  • assert_success — $statusが0であることをアサートします。
  • assert_failure — $statusがゼロ以外であることをアサートします。
  • assert_output — $outputが指定した文字列と一致することをアサートします。
  • assert_output --partial — 出力に部分文字列が含まれることをアサートします。
  • refute_output --partial — 出力に部分文字列が含まれないことをアサートします。

bats-core/bats-assertをtest/helpers/フォルダにcloneしてインストールし、その後でloadします。

#!/usr/bin/env bats

# Load bats-assert (cloned into test/helpers/bats-assert)
load 'helpers/bats-assert/load'
load '../lib/math.sh'

@test "add 5 and 3 gives 8" {
  run add 5 3
  assert_success
  assert_output "8"
}

@test "divide by zero fails with descriptive message" {
  run divide 9 0
  assert_failure
  assert_output --partial "division by zero"
}

@test "add does not output an error" {
  run add 1 1
  refute_output --partial "Error"
}

setupとteardown:テストライフサイクルフック

Batsには、各テストの前後に自動的に実行される2つの特別な関数、setupとteardownがあります。これらを使って共有状態の準備と後片付けを行い、各テストが既知の環境から開始できるようにします。

  • setup()は、個々の@testブロックの前に実行されます。
  • teardown()は、個々の@testブロックの後に実行されます。テストが失敗した場合も実行されます。
  • よくある用途は、一時ディレクトリの作成、環境変数の設定、テスト後の一時ファイルの削除です。
#!/usr/bin/env bats

load '../lib/fileutils.sh'

setup() {
  # Create a fresh temp directory before every test
  TEST_DIR=$(mktemp -d)
  export TEST_DIR
}

teardown() {
  # Always clean up, even on test failure
  rm -rf "$TEST_DIR"
}

@test "write_file creates a file with correct content" {
  run write_file "$TEST_DIR/hello.txt" "hello world"
  assert_success
  [ -f "$TEST_DIR/hello.txt" ]
  [ "$(cat "$TEST_DIR/hello.txt")" = "hello world" ]
}

@test "write_file fails when directory does not exist" {
  run write_file "/nonexistent/dir/file.txt" "data"
  assert_failure
}

setup_fileとteardown_file:テストスイート単位のフック

すべてのテストの前ではなく、ファイルごとに一度だけ高コストなリソースを準備したい場合があります。そのためにBatsではsetup_fileとteardown_fileを利用できます。

  • setup_file()は、ファイル内のすべてのテストの前に一度だけ実行されます。
  • teardown_file()は、ファイル内のすべてのテストの後に一度だけ実行されます。
  • 自動的に利用できるBATS_FILE_TMPDIRを使うと、setup_fileとテストの間でデータを共有できます。通常の変数はサブシェルをまたいで保持されません。

典型的な用途は、モックサーバーの起動やバイナリのビルドを一度だけ行い、最後に停止・削除することです。

#!/usr/bin/env bats

setup_file() {
  # Build the project binary once for all tests in this file
  make build --silent
  export BINARY="$PWD/bin/myapp"
  echo "Binary built: $BINARY"
}

teardown_file() {
  # Remove the binary after all tests complete
  rm -f "$BINARY"
  echo "Cleaned up binary"
}

setup() {
  # Still runs before each individual test
  TEST_TMP=$(mktemp -d)
}

teardown() {
  rm -rf "$TEST_TMP"
}

@test "myapp --version outputs version string" {
  run "$BINARY" --version
  assert_output --partial "1.0"
}

ファイルを変更する関数をテストする

Bash関数がファイルシステムから読み取ったり、ファイルシステムへ書き込んだりする処理をテストするのは、非常によくあるパターンです。重要なテクニックは、setup内でmktemp -dを使って一時ディレクトリを作成することです。これにより、テストが実際のファイルに触れたり、互いに干渉したりすることを防げます。

  • 常に$TEST_DIR(または最近のBatsで自動的に利用できる$BATS_TEST_TMPDIR)内で作業します。
  • assert_file_existsやassert_file_containsのような、分かりやすいファイルアサーションにはbats-fileヘルパーライブラリを使います。
  • /tmp/myfileのようなパスをハードコードしないでください。並列テストの実行時に衝突します。
#!/usr/bin/env bats

load 'helpers/bats-assert/load'
load 'helpers/bats-file/load'
load '../lib/fileutils.sh'

setup() {
  TEST_DIR="$BATS_TEST_TMPDIR"
}

# lib/fileutils.sh defines:
# append_line() { echo "$2" >> "$1"; }

@test "append_line adds a line to an existing file" {
  echo "first line" > "$TEST_DIR/log.txt"

  run append_line "$TEST_DIR/log.txt" "second line"
  assert_success

  assert_file_contains "$TEST_DIR/log.txt" "second line"
}

@test "append_line creates file if it does not exist" {
  run append_line "$TEST_DIR/new.txt" "hello"
  assert_success
  assert_file_exists "$TEST_DIR/new.txt"
}

外部コマンドをモックする

関数では、curl、aws、gitなどの外部プログラムを呼び出すことがよくあります。ユニットテストでは、実際の外部コマンドではなく、自分のロジックをテストしたいものです。Batsで最もシンプルにモックする方法は、setup内でコマンドと同じ名前のシェル関数を定義することです。これにより、その関数が実際のバイナリより優先されます。

  • setup内でcurl() { echo 'mocked response'; return 0; }のような関数を定義し、exportします。
  • export -f curlを使うと、runが起動するサブシェルからも関数を利用できます。
  • より複雑なケースでは、PATH上の一時ファイルにモックを書き込むこともできます。
#!/usr/bin/env bats

load 'helpers/bats-assert/load'
load '../lib/network.sh'

# lib/network.sh defines:
# fetch_status() {
#   local url="$1"
#   local code
#   code=$(curl -s -o /dev/null -w "%{http_code}" "$url")
#   echo "$code"
# }

setup() {
  # Override 'curl' with a mock function
  curl() {
    # Simulate a 200 OK response
    echo "200"
    return 0
  }
  export -f curl
}

@test "fetch_status returns 200 when curl reports 200" {
  run fetch_status "https://example.com"
  assert_success
  assert_output "200"
}

テストのスキップとタグ付け

すべてのテストを常に実行できるとは限りません。実ネットワークへの接続、特定のツール、特定のOSが必要になる場合があります。Batsのskipを使うと、テストをコメントアウトしたりテストスイートを壊したりせず、説明メッセージを付けて条件付きでスキップできます。

  • @testブロック内の任意の場所でskip "reason"を呼び出すと、そのテストをスキップできます。
  • スキップされたテストは出力にSとして表示され、失敗には数えられません。
  • Bats 1.5以降ではタグに対応しています。# bats test_tags=slow,networkでテストにタグを付け、bats --filter-tags network test/で絞り込めます。
#!/usr/bin/env bats

# bats test_tags=network
@test "API returns valid JSON" {
  # Skip if no internet connectivity
  if ! ping -c1 -W1 8.8.8.8 &>/dev/null; then
    skip "No network connection available"
  fi

  run curl -s "https://api.example.com/health"
  assert_success
  assert_output --partial '"status"'
}

# bats test_tags=unit
@test "slug function lowercases and replaces spaces" {
  # Always runs — pure function, no external deps
  slug() { echo "$1" | tr '[:upper:]' '[:lower:]' | tr ' ' '-'; }
  run slug "Hello World"
  assert_output "hello-world"
}

# Run only unit tests:
# bats --filter-tags unit test/

テストスイート全体を構成する

適切に整理されたBatsプロジェクトでは、予測しやすいディレクトリ構成を採用します。これにより、新しいコントリビューターが参加しやすくなり、CIパイプラインにも簡単に統合できます。

推奨される構成は次のとおりです。

  • lib/ — 本番用のBash関数を置きます。関心ごとごとに1ファイルに分けます(math.sh、fileutils.shなど)。
  • test/ — ライブラリファイルごとに1つの.batsファイルを置きます(math.bats、fileutils.batsなど)。
  • test/helpers/ — bats-assert、bats-file、bats-supportをGitサブモジュールとして置きます。
  • Makefile — testターゲットを用意し、コントリビューターがmake testを実行するだけで済むようにします。

次の1つのコマンドでテストスイート全体を実行できます。

# Makefile
.PHONY: test
test:
	bats test/

# Run all tests recursively (Bats 1.5+)
# bats --recursive test/

# Run a specific file
# bats test/math.bats

# Run with verbose (TAP) output for CI
# bats --tap test/

# Example directory tree:
# .
# |-- lib/
# |   |-- math.sh
# |   `-- fileutils.sh
# |-- test/
# |   |-- helpers/
# |   |   |-- bats-assert/
# |   |   `-- bats-file/
# |   |-- math.bats
# |   `-- fileutils.bats
# `-- Makefile

理解度チェック:Bats-coreのアサーション

Bats-coreの基本的なテストの仕組みについて、理解度を確認しましょう。

まとめ:BatsによるBashのユニットテスト

このレッスンでは、Bats-coreを使って個々のBash関数のユニットテストを構成・記述する方法を学びました。重要なポイントをまとめます。

  • テストファイルの構成 — #!/usr/bin/env batsのshebangと、説明的な名前を付けた@testブロックを使います。
  • load — ライブラリファイルをsourceし、コピー&ペーストせずにテストから関数を利用できるようにします。
  • run + $status + $output — 中心となる3つの要素です。結果を取得しつつテストをただちに失敗させないため、常にrunを使います。
  • bats-assert — 読みやすい失敗メッセージを得るため、通常の[ ]よりもassert_success、assert_failure、assert_outputを優先します。
  • setup / teardown — 各テストの前後に実行されます。setup_file / teardown_fileはファイルごとに一度だけ実行されます。
  • モック — 同じ名前のシェル関数をexport -fでexportし、外部コマンドを置き換えます。
  • skip — 利用できないリソースに依存するテストを条件付きでスキップします。
  • プロジェクト構成 — 保守性とCI統合のため、lib/、test/、test/helpers/を分けて管理します。

これらのパターンにより、Bashプロジェクトにも、最新のソフトウェアプロジェクトに適用するのと同じテスト規律を取り入れられます。

よくある質問

「Bats-coreによる関数の単体テスト」レッスンは無料ですか?

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

「Bats-coreによる関数の単体テスト」で何を学びますか?

テストファイル、アサーション、セットアップとティアダウンを構成し、個々のBash関数を検証します。 ブラウザで直接実行するハンズオンコードでLinux Command Line & Bash Scripting Masteryを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

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

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

「Bats-coreによる関数の単体テスト」レッスンにはどのくらい時間がかかりますか?

ほとんどの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に戻る