Bats Testing Patterns

Write bulletproof tests for shell scripts with Bats framework mastery

✨ The solution you've been looking for

Verified
Tested and verified by our team
25450 Stars

Master Bash Automated Testing System (Bats) for comprehensive shell script testing. Use when writing tests for shell scripts, CI/CD pipelines, or requiring test-driven development of shell utilities.

shell-scripting testing automation ci-cd bash test-driven-development quality-assurance devops
Repository

See It In Action

Interactive preview & real-world examples

Live Demo
Skill Demo Animation

AI Conversation Simulator

See how users interact with this skill

User Prompt

Help me write Bats tests for a shell script that processes log files. I need to test file validation, parsing logic, and error conditions.

Skill Processing

Analyzing request...

Agent Response

Complete test suite with setup/teardown, fixtures, mocking patterns, and comprehensive coverage of success and failure cases

Quick Start (3 Steps)

Get up and running in minutes

1

Install

claude-code skill install bats-testing-patterns

claude-code skill install bats-testing-patterns
2

Config

3

First Trigger

@bats-testing-patterns help

Commands

CommandDescriptionRequired Args
@bats-testing-patterns test-driven-shell-script-developmentWrite comprehensive unit tests for shell utilities before and during developmentNone
@bats-testing-patterns ci/cd-pipeline-testingIntegrate Bats tests into automated build pipelines for shell-based deployment scriptsNone
@bats-testing-patterns legacy-script-validationAdd comprehensive testing to existing shell scripts to prevent regressions during maintenanceNone

Typical Use Cases

Test-Driven Shell Script Development

Write comprehensive unit tests for shell utilities before and during development

CI/CD Pipeline Testing

Integrate Bats tests into automated build pipelines for shell-based deployment scripts

Legacy Script Validation

Add comprehensive testing to existing shell scripts to prevent regressions during maintenance

Overview

Bats Testing Patterns

Comprehensive guidance for writing comprehensive unit tests for shell scripts using Bats (Bash Automated Testing System), including test patterns, fixtures, and best practices for production-grade shell testing.

When to Use This Skill

  • Writing unit tests for shell scripts
  • Implementing test-driven development (TDD) for scripts
  • Setting up automated testing in CI/CD pipelines
  • Testing edge cases and error conditions
  • Validating behavior across different shell environments
  • Building maintainable test suites for scripts
  • Creating fixtures for complex test scenarios
  • Testing multiple shell dialects (bash, sh, dash)

Bats Fundamentals

What is Bats?

Bats (Bash Automated Testing System) is a TAP (Test Anything Protocol) compliant testing framework for shell scripts that provides:

  • Simple, natural test syntax
  • TAP output format compatible with CI systems
  • Fixtures and setup/teardown support
  • Assertion helpers
  • Parallel test execution

Installation

 1# macOS with Homebrew
 2brew install bats-core
 3
 4# Ubuntu/Debian
 5git clone https://github.com/bats-core/bats-core.git
 6cd bats-core
 7./install.sh /usr/local
 8
 9# From npm (Node.js)
10npm install --global bats
11
12# Verify installation
13bats --version

File Structure

project/
├── bin/
│   ├── script.sh
│   └── helper.sh
├── tests/
│   ├── test_script.bats
│   ├── test_helper.sh
│   ├── fixtures/
│   │   ├── input.txt
│   │   └── expected_output.txt
│   └── helpers/
│       └── mocks.bash
└── README.md

Basic Test Structure

Simple Test File

 1#!/usr/bin/env bats
 2
 3# Load test helper if present
 4load test_helper
 5
 6# Setup runs before each test
 7setup() {
 8    export TMPDIR=$(mktemp -d)
 9}
10
11# Teardown runs after each test
12teardown() {
13    rm -rf "$TMPDIR"
14}
15
16# Test: simple assertion
17@test "Function returns 0 on success" {
18    run my_function "input"
19    [ "$status" -eq 0 ]
20}
21
22# Test: output verification
23@test "Function outputs correct result" {
24    run my_function "test"
25    [ "$output" = "expected output" ]
26}
27
28# Test: error handling
29@test "Function returns 1 on missing argument" {
30    run my_function
31    [ "$status" -eq 1 ]
32}

Assertion Patterns

Exit Code Assertions

 1#!/usr/bin/env bats
 2
 3@test "Command succeeds" {
 4    run true
 5    [ "$status" -eq 0 ]
 6}
 7
 8@test "Command fails as expected" {
 9    run false
10    [ "$status" -ne 0 ]
11}
12
13@test "Command returns specific exit code" {
14    run my_function --invalid
15    [ "$status" -eq 127 ]
16}
17
18@test "Can capture command result" {
19    run echo "hello"
20    [ $status -eq 0 ]
21    [ "$output" = "hello" ]
22}

Output Assertions

 1#!/usr/bin/env bats
 2
 3@test "Output matches string" {
 4    result=$(echo "hello world")
 5    [ "$result" = "hello world" ]
 6}
 7
 8@test "Output contains substring" {
 9    result=$(echo "hello world")
10    [[ "$result" == *"world"* ]]
11}
12
13@test "Output matches pattern" {
14    result=$(date +%Y)
15    [[ "$result" =~ ^[0-9]{4}$ ]]
16}
17
18@test "Multi-line output" {
19    run printf "line1\nline2\nline3"
20    [ "$output" = "line1
21line2
22line3" ]
23}
24
25@test "Lines variable contains output" {
26    run printf "line1\nline2\nline3"
27    [ "${lines[0]}" = "line1" ]
28    [ "${lines[1]}" = "line2" ]
29    [ "${lines[2]}" = "line3" ]
30}

File Assertions

 1#!/usr/bin/env bats
 2
 3@test "File is created" {
 4    [ ! -f "$TMPDIR/output.txt" ]
 5    my_function > "$TMPDIR/output.txt"
 6    [ -f "$TMPDIR/output.txt" ]
 7}
 8
 9@test "File contents match expected" {
10    my_function > "$TMPDIR/output.txt"
11    [ "$(cat "$TMPDIR/output.txt")" = "expected content" ]
12}
13
14@test "File is readable" {
15    touch "$TMPDIR/test.txt"
16    [ -r "$TMPDIR/test.txt" ]
17}
18
19@test "File has correct permissions" {
20    touch "$TMPDIR/test.txt"
21    chmod 644 "$TMPDIR/test.txt"
22    [ "$(stat -f %OLp "$TMPDIR/test.txt")" = "644" ]
23}
24
25@test "File size is correct" {
26    echo -n "12345" > "$TMPDIR/test.txt"
27    [ "$(wc -c < "$TMPDIR/test.txt")" -eq 5 ]
28}

Setup and Teardown Patterns

Basic Setup and Teardown

 1#!/usr/bin/env bats
 2
 3setup() {
 4    # Create test directory
 5    TEST_DIR=$(mktemp -d)
 6    export TEST_DIR
 7
 8    # Source script under test
 9    source "${BATS_TEST_DIRNAME}/../bin/script.sh"
10}
11
12teardown() {
13    # Clean up temporary directory
14    rm -rf "$TEST_DIR"
15}
16
17@test "Test using TEST_DIR" {
18    touch "$TEST_DIR/file.txt"
19    [ -f "$TEST_DIR/file.txt" ]
20}

Setup with Resources

 1#!/usr/bin/env bats
 2
 3setup() {
 4    # Create directory structure
 5    mkdir -p "$TMPDIR/data/input"
 6    mkdir -p "$TMPDIR/data/output"
 7
 8    # Create test fixtures
 9    echo "line1" > "$TMPDIR/data/input/file1.txt"
10    echo "line2" > "$TMPDIR/data/input/file2.txt"
11
12    # Initialize environment
13    export DATA_DIR="$TMPDIR/data"
14    export INPUT_DIR="$DATA_DIR/input"
15    export OUTPUT_DIR="$DATA_DIR/output"
16}
17
18teardown() {
19    rm -rf "$TMPDIR/data"
20}
21
22@test "Processes input files" {
23    run my_process_script "$INPUT_DIR" "$OUTPUT_DIR"
24    [ "$status" -eq 0 ]
25    [ -f "$OUTPUT_DIR/file1.txt" ]
26}

Global Setup/Teardown

 1#!/usr/bin/env bats
 2
 3# Load shared setup from test_helper.sh
 4load test_helper
 5
 6# setup_file runs once before all tests
 7setup_file() {
 8    export SHARED_RESOURCE=$(mktemp -d)
 9    echo "Expensive setup" > "$SHARED_RESOURCE/data.txt"
10}
11
12# teardown_file runs once after all tests
13teardown_file() {
14    rm -rf "$SHARED_RESOURCE"
15}
16
17@test "First test uses shared resource" {
18    [ -f "$SHARED_RESOURCE/data.txt" ]
19}
20
21@test "Second test uses shared resource" {
22    [ -d "$SHARED_RESOURCE" ]
23}

Mocking and Stubbing Patterns

Function Mocking

 1#!/usr/bin/env bats
 2
 3# Mock external command
 4my_external_tool() {
 5    echo "mocked output"
 6    return 0
 7}
 8
 9@test "Function uses mocked tool" {
10    export -f my_external_tool
11    run my_function
12    [[ "$output" == *"mocked output"* ]]
13}

Command Stubbing

 1#!/usr/bin/env bats
 2
 3setup() {
 4    # Create stub directory
 5    STUBS_DIR="$TMPDIR/stubs"
 6    mkdir -p "$STUBS_DIR"
 7
 8    # Add to PATH
 9    export PATH="$STUBS_DIR:$PATH"
10}
11
12create_stub() {
13    local cmd="$1"
14    local output="$2"
15    local code="${3:-0}"
16
17    cat > "$STUBS_DIR/$cmd" <<EOF
18#!/bin/bash
19echo "$output"
20exit $code
21EOF
22    chmod +x "$STUBS_DIR/$cmd"
23}
24
25@test "Function works with stubbed curl" {
26    create_stub curl "{ \"status\": \"ok\" }" 0
27    run my_api_function
28    [ "$status" -eq 0 ]
29}

Variable Stubbing

 1#!/usr/bin/env bats
 2
 3@test "Function handles environment override" {
 4    export MY_SETTING="override_value"
 5    run my_function
 6    [ "$status" -eq 0 ]
 7    [[ "$output" == *"override_value"* ]]
 8}
 9
10@test "Function uses default when var unset" {
11    unset MY_SETTING
12    run my_function
13    [ "$status" -eq 0 ]
14    [[ "$output" == *"default"* ]]
15}

Fixture Management

Using Fixture Files

 1#!/usr/bin/env bats
 2
 3# Fixture directory: tests/fixtures/
 4
 5setup() {
 6    FIXTURES_DIR="${BATS_TEST_DIRNAME}/fixtures"
 7    WORK_DIR=$(mktemp -d)
 8    export WORK_DIR
 9}
10
11teardown() {
12    rm -rf "$WORK_DIR"
13}
14
15@test "Process fixture file" {
16    # Copy fixture to work directory
17    cp "$FIXTURES_DIR/input.txt" "$WORK_DIR/input.txt"
18
19    # Run function
20    run my_process_function "$WORK_DIR/input.txt"
21
22    # Compare output
23    diff "$WORK_DIR/output.txt" "$FIXTURES_DIR/expected_output.txt"
24}

Dynamic Fixture Generation

 1#!/usr/bin/env bats
 2
 3generate_fixture() {
 4    local lines="$1"
 5    local file="$2"
 6
 7    for i in $(seq 1 "$lines"); do
 8        echo "Line $i content" >> "$file"
 9    done
10}
11
12@test "Handle large input file" {
13    generate_fixture 1000 "$TMPDIR/large.txt"
14    run my_function "$TMPDIR/large.txt"
15    [ "$status" -eq 0 ]
16    [ "$(wc -l < "$TMPDIR/large.txt")" -eq 1000 ]
17}

Advanced Patterns

Testing Error Conditions

 1#!/usr/bin/env bats
 2
 3@test "Function fails with missing file" {
 4    run my_function "/nonexistent/file.txt"
 5    [ "$status" -ne 0 ]
 6    [[ "$output" == *"not found"* ]]
 7}
 8
 9@test "Function fails with invalid input" {
10    run my_function ""
11    [ "$status" -ne 0 ]
12}
13
14@test "Function fails with permission denied" {
15    touch "$TMPDIR/readonly.txt"
16    chmod 000 "$TMPDIR/readonly.txt"
17    run my_function "$TMPDIR/readonly.txt"
18    [ "$status" -ne 0 ]
19    chmod 644 "$TMPDIR/readonly.txt"  # Cleanup
20}
21
22@test "Function provides helpful error message" {
23    run my_function --invalid-option
24    [ "$status" -ne 0 ]
25    [[ "$output" == *"Usage:"* ]]
26}

Testing with Dependencies

 1#!/usr/bin/env bats
 2
 3setup() {
 4    # Check for required tools
 5    if ! command -v jq &>/dev/null; then
 6        skip "jq is not installed"
 7    fi
 8
 9    export SCRIPT="${BATS_TEST_DIRNAME}/../bin/script.sh"
10}
11
12@test "JSON parsing works" {
13    skip_if ! command -v jq &>/dev/null
14    run my_json_parser '{"key": "value"}'
15    [ "$status" -eq 0 ]
16}

Testing Shell Compatibility

 1#!/usr/bin/env bats
 2
 3@test "Script works in bash" {
 4    bash "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1
 5}
 6
 7@test "Script works in sh (POSIX)" {
 8    sh "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1
 9}
10
11@test "Script works in dash" {
12    if command -v dash &>/dev/null; then
13        dash "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1
14    else
15        skip "dash not installed"
16    fi
17}

Parallel Execution

 1#!/usr/bin/env bats
 2
 3@test "Multiple independent operations" {
 4    run bash -c 'for i in {1..10}; do
 5        my_operation "$i" &
 6    done
 7    wait'
 8    [ "$status" -eq 0 ]
 9}
10
11@test "Concurrent file operations" {
12    for i in {1..5}; do
13        my_function "$TMPDIR/file$i" &
14    done
15    wait
16    [ -f "$TMPDIR/file1" ]
17    [ -f "$TMPDIR/file5" ]
18}

Test Helper Pattern

test_helper.sh

 1#!/usr/bin/env bash
 2
 3# Source script under test
 4export SCRIPT_DIR="${BATS_TEST_DIRNAME%/*}/bin"
 5
 6# Common test utilities
 7assert_file_exists() {
 8    if [ ! -f "$1" ]; then
 9        echo "Expected file to exist: $1"
10        return 1
11    fi
12}
13
14assert_file_equals() {
15    local file="$1"
16    local expected="$2"
17
18    if [ ! -f "$file" ]; then
19        echo "File does not exist: $file"
20        return 1
21    fi
22
23    local actual=$(cat "$file")
24    if [ "$actual" != "$expected" ]; then
25        echo "File contents do not match"
26        echo "Expected: $expected"
27        echo "Actual: $actual"
28        return 1
29    fi
30}
31
32# Create temporary test directory
33setup_test_dir() {
34    export TEST_DIR=$(mktemp -d)
35}
36
37cleanup_test_dir() {
38    rm -rf "$TEST_DIR"
39}

Integration with CI/CD

GitHub Actions Workflow

 1name: Tests
 2
 3on: [push, pull_request]
 4
 5jobs:
 6  test:
 7    runs-on: ubuntu-latest
 8
 9    steps:
10      - uses: actions/checkout@v3
11
12      - name: Install Bats
13        run: |
14          npm install --global bats
15
16      - name: Run Tests
17        run: |
18          bats tests/*.bats
19
20      - name: Run Tests with Tap Reporter
21        run: |
22          bats tests/*.bats --tap | tee test_output.tap

Makefile Integration

 1.PHONY: test test-verbose test-tap
 2
 3test:
 4	bats tests/*.bats
 5
 6test-verbose:
 7	bats tests/*.bats --verbose
 8
 9test-tap:
10	bats tests/*.bats --tap
11
12test-parallel:
13	bats tests/*.bats --parallel 4
14
15coverage: test
16	# Optional: Generate coverage reports

Best Practices

  1. Test one thing per test - Single responsibility principle
  2. Use descriptive test names - Clearly states what is being tested
  3. Clean up after tests - Always remove temporary files in teardown
  4. Test both success and failure paths - Don’t just test happy path
  5. Mock external dependencies - Isolate unit under test
  6. Use fixtures for complex data - Makes tests more readable
  7. Run tests in CI/CD - Catch regressions early
  8. Test across shell dialects - Ensure portability
  9. Keep tests fast - Run in parallel when possible
  10. Document complex test setup - Explain unusual patterns

Resources

What Users Are Saying

Real feedback from the community

Environment Matrix

Dependencies

Bats-core testing framework
Bash 4.0+ or compatible shell
Standard UNIX utilities (mktemp, wc, stat)

Framework Support

Bats-core ✓ (recommended) TAP (Test Anything Protocol) ✓ GitHub Actions ✓ Jenkins ✓ Make ✓

Context Window

Token Usage ~3K-8K tokens depending on test complexity and fixture size

Security & Privacy

Information

Author
wshobson
Updated
2026-01-30
Category
automation-tools