Claude Cursor Skill

bats-testing-patterns

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.

LLM Mart · 0 points · 13 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download wshobson-agents-plugins_shell-scripting_skills_bats-testing-patterns-554237f.zip · 4 KB
Part of wshobson/agents — 170 skills

Install

skills CLI npx skills add https://github.com/wshobson/agents/tree/main/plugins/shell-scripting/skills/bats-testing-patterns
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install wshobson-agents@llmmart
Git git clone https://github.com/wshobson/agents.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole wshobson/agents collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

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)

Detailed patterns and worked examples

Detailed pattern documentation lives in references/details.md. Read that file when the navigation tier above is insufficient.

Testing Error Conditions

#!/usr/bin/env bats

@test "Function fails with missing file" {
    run my_function "/nonexistent/file.txt"
    [ "$status" -ne 0 ]
    [[ "$output" == *"not found"* ]]
}

@test "Function fails with invalid input" {
    run my_function ""
    [ "$status" -ne 0 ]
}

@test "Function fails with permission denied" {
    touch "$TMPDIR/readonly.txt"
    chmod 000 "$TMPDIR/readonly.txt"
    run my_function "$TMPDIR/readonly.txt"
    [ "$status" -ne 0 ]
    chmod 644 "$TMPDIR/readonly.txt"  # Cleanup
}

@test "Function provides helpful error message" {
    run my_function --invalid-option
    [ "$status" -ne 0 ]
    [[ "$output" == *"Usage:"* ]]
}

Testing with Dependencies

#!/usr/bin/env bats

setup() {
    # Check for required tools
    if ! command -v jq &>/dev/null; then
        skip "jq is not installed"
    fi

    export SCRIPT="${BATS_TEST_DIRNAME}/../bin/script.sh"
}

@test "JSON parsing works" {
    skip_if ! command -v jq &>/dev/null
    run my_json_parser '{"key": "value"}'
    [ "$status" -eq 0 ]
}

Testing Shell Compatibility

#!/usr/bin/env bats

@test "Script works in bash" {
    bash "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1
}

@test "Script works in sh (POSIX)" {
    sh "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1
}

@test "Script works in dash" {
    if command -v dash &>/dev/null; then
        dash "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1
    else
        skip "dash not installed"
    fi
}

Parallel Execution

#!/usr/bin/env bats

@test "Multiple independent operations" {
    run bash -c 'for i in {1..10}; do
        my_operation "$i" &
    done
    wait'
    [ "$status" -eq 0 ]
}

@test "Concurrent file operations" {
    for i in {1..5}; do
        my_function "$TMPDIR/file$i" &
    done
    wait
    [ -f "$TMPDIR/file1" ]
    [ -f "$TMPDIR/file5" ]
}

Test Helper Pattern

test_helper.sh

#!/usr/bin/env bash

# Source script under test
export SCRIPT_DIR="${BATS_TEST_DIRNAME%/*}/bin"

# Common test utilities
assert_file_exists() {
    if [ ! -f "$1" ]; then
        echo "Expected file to exist: $1"
        return 1
    fi
}

assert_file_equals() {
    local file="$1"
    local expected="$2"

    if [ ! -f "$file" ]; then
        echo "File does not exist: $file"
        return 1
    fi

    local actual=$(cat "$file")
    if [ "$actual" != "$expected" ]; then
        echo "File contents do not match"
        echo "Expected: $expected"
        echo "Actual: $actual"
        return 1
    fi
}

# Create temporary test directory
setup_test_dir() {
    export TEST_DIR=$(mktemp -d)
}

cleanup_test_dir() {
    rm -rf "$TEST_DIR"
}

Integration with CI/CD

GitHub Actions Workflow

name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Install Bats
        run: |
          npm install --global bats

      - name: Run Tests
        run: |
          bats tests/*.bats

      - name: Run Tests with Tap Reporter
        run: |
          bats tests/*.bats --tap | tee test_output.tap

Makefile Integration

.PHONY: test test-verbose test-tap

test:
	bats tests/*.bats

test-verbose:
	bats tests/*.bats --verbose

test-tap:
	bats tests/*.bats --tap

test-parallel:
	bats tests/*.bats --parallel 4

coverage: test
	# 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
Files (agents)
  • references
    • details.md 7.1 KB
      # bats-testing-patterns — detailed patterns and worked examples
      
      ## 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
      
      ```bash
      # macOS with Homebrew
      brew install bats-core
      
      # Ubuntu/Debian
      git clone https://github.com/bats-core/bats-core.git
      cd bats-core
      ./install.sh /usr/local
      
      # From npm (Node.js)
      npm install --global bats
      
      # Verify installation
      bats --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
      
      ```bash
      #!/usr/bin/env bats
      
      # Load test helper if present
      load test_helper
      
      # Setup runs before each test
      setup() {
          export TMPDIR=$(mktemp -d)
      }
      
      # Teardown runs after each test
      teardown() {
          rm -rf "$TMPDIR"
      }
      
      # Test: simple assertion
      @test "Function returns 0 on success" {
          run my_function "input"
          [ "$status" -eq 0 ]
      }
      
      # Test: output verification
      @test "Function outputs correct result" {
          run my_function "test"
          [ "$output" = "expected output" ]
      }
      
      # Test: error handling
      @test "Function returns 1 on missing argument" {
          run my_function
          [ "$status" -eq 1 ]
      }
      ```
      
      ## Assertion Patterns
      
      ### Exit Code Assertions
      
      ```bash
      #!/usr/bin/env bats
      
      @test "Command succeeds" {
          run true
          [ "$status" -eq 0 ]
      }
      
      @test "Command fails as expected" {
          run false
          [ "$status" -ne 0 ]
      }
      
      @test "Command returns specific exit code" {
          run my_function --invalid
          [ "$status" -eq 127 ]
      }
      
      @test "Can capture command result" {
          run echo "hello"
          [ $status -eq 0 ]
          [ "$output" = "hello" ]
      }
      ```
      
      ### Output Assertions
      
      ```bash
      #!/usr/bin/env bats
      
      @test "Output matches string" {
          result=$(echo "hello world")
          [ "$result" = "hello world" ]
      }
      
      @test "Output contains substring" {
          result=$(echo "hello world")
          [[ "$result" == *"world"* ]]
      }
      
      @test "Output matches pattern" {
          result=$(date +%Y)
          [[ "$result" =~ ^[0-9]{4}$ ]]
      }
      
      @test "Multi-line output" {
          run printf "line1\nline2\nline3"
          [ "$output" = "line1
      line2
      line3" ]
      }
      
      @test "Lines variable contains output" {
          run printf "line1\nline2\nline3"
          [ "${lines[0]}" = "line1" ]
          [ "${lines[1]}" = "line2" ]
          [ "${lines[2]}" = "line3" ]
      }
      ```
      
      ### File Assertions
      
      ```bash
      #!/usr/bin/env bats
      
      @test "File is created" {
          [ ! -f "$TMPDIR/output.txt" ]
          my_function > "$TMPDIR/output.txt"
          [ -f "$TMPDIR/output.txt" ]
      }
      
      @test "File contents match expected" {
          my_function > "$TMPDIR/output.txt"
          [ "$(cat "$TMPDIR/output.txt")" = "expected content" ]
      }
      
      @test "File is readable" {
          touch "$TMPDIR/test.txt"
          [ -r "$TMPDIR/test.txt" ]
      }
      
      @test "File has correct permissions" {
          touch "$TMPDIR/test.txt"
          chmod 644 "$TMPDIR/test.txt"
          [ "$(stat -f %OLp "$TMPDIR/test.txt")" = "644" ]
      }
      
      @test "File size is correct" {
          echo -n "12345" > "$TMPDIR/test.txt"
          [ "$(wc -c < "$TMPDIR/test.txt")" -eq 5 ]
      }
      ```
      
      ## Setup and Teardown Patterns
      
      ### Basic Setup and Teardown
      
      ```bash
      #!/usr/bin/env bats
      
      setup() {
          # Create test directory
          TEST_DIR=$(mktemp -d)
          export TEST_DIR
      
          # Source script under test
          source "${BATS_TEST_DIRNAME}/../bin/script.sh"
      }
      
      teardown() {
          # Clean up temporary directory
          rm -rf "$TEST_DIR"
      }
      
      @test "Test using TEST_DIR" {
          touch "$TEST_DIR/file.txt"
          [ -f "$TEST_DIR/file.txt" ]
      }
      ```
      
      ### Setup with Resources
      
      ```bash
      #!/usr/bin/env bats
      
      setup() {
          # Create directory structure
          mkdir -p "$TMPDIR/data/input"
          mkdir -p "$TMPDIR/data/output"
      
          # Create test fixtures
          echo "line1" > "$TMPDIR/data/input/file1.txt"
          echo "line2" > "$TMPDIR/data/input/file2.txt"
      
          # Initialize environment
          export DATA_DIR="$TMPDIR/data"
          export INPUT_DIR="$DATA_DIR/input"
          export OUTPUT_DIR="$DATA_DIR/output"
      }
      
      teardown() {
          rm -rf "$TMPDIR/data"
      }
      
      @test "Processes input files" {
          run my_process_script "$INPUT_DIR" "$OUTPUT_DIR"
          [ "$status" -eq 0 ]
          [ -f "$OUTPUT_DIR/file1.txt" ]
      }
      ```
      
      ### Global Setup/Teardown
      
      ```bash
      #!/usr/bin/env bats
      
      # Load shared setup from test_helper.sh
      load test_helper
      
      # setup_file runs once before all tests
      setup_file() {
          export SHARED_RESOURCE=$(mktemp -d)
          echo "Expensive setup" > "$SHARED_RESOURCE/data.txt"
      }
      
      # teardown_file runs once after all tests
      teardown_file() {
          rm -rf "$SHARED_RESOURCE"
      }
      
      @test "First test uses shared resource" {
          [ -f "$SHARED_RESOURCE/data.txt" ]
      }
      
      @test "Second test uses shared resource" {
          [ -d "$SHARED_RESOURCE" ]
      }
      ```
      
      ## Mocking and Stubbing Patterns
      
      ### Function Mocking
      
      ```bash
      #!/usr/bin/env bats
      
      # Mock external command
      my_external_tool() {
          echo "mocked output"
          return 0
      }
      
      @test "Function uses mocked tool" {
          export -f my_external_tool
          run my_function
          [[ "$output" == *"mocked output"* ]]
      }
      ```
      
      ### Command Stubbing
      
      ```bash
      #!/usr/bin/env bats
      
      setup() {
          # Create stub directory
          STUBS_DIR="$TMPDIR/stubs"
          mkdir -p "$STUBS_DIR"
      
          # Add to PATH
          export PATH="$STUBS_DIR:$PATH"
      }
      
      create_stub() {
          local cmd="$1"
          local output="$2"
          local code="${3:-0}"
      
          cat > "$STUBS_DIR/$cmd" <<EOF
      #!/bin/bash
      echo "$output"
      exit $code
      EOF
          chmod +x "$STUBS_DIR/$cmd"
      }
      
      @test "Function works with stubbed curl" {
          create_stub curl "{ \"status\": \"ok\" }" 0
          run my_api_function
          [ "$status" -eq 0 ]
      }
      ```
      
      ### Variable Stubbing
      
      ```bash
      #!/usr/bin/env bats
      
      @test "Function handles environment override" {
          export MY_SETTING="override_value"
          run my_function
          [ "$status" -eq 0 ]
          [[ "$output" == *"override_value"* ]]
      }
      
      @test "Function uses default when var unset" {
          unset MY_SETTING
          run my_function
          [ "$status" -eq 0 ]
          [[ "$output" == *"default"* ]]
      }
      ```
      
      ## Fixture Management
      
      ### Using Fixture Files
      
      ```bash
      #!/usr/bin/env bats
      
      # Fixture directory: tests/fixtures/
      
      setup() {
          FIXTURES_DIR="${BATS_TEST_DIRNAME}/fixtures"
          WORK_DIR=$(mktemp -d)
          export WORK_DIR
      }
      
      teardown() {
          rm -rf "$WORK_DIR"
      }
      
      @test "Process fixture file" {
          # Copy fixture to work directory
          cp "$FIXTURES_DIR/input.txt" "$WORK_DIR/input.txt"
      
          # Run function
          run my_process_function "$WORK_DIR/input.txt"
      
          # Compare output
          diff "$WORK_DIR/output.txt" "$FIXTURES_DIR/expected_output.txt"
      }
      ```
      
      ### Dynamic Fixture Generation
      
      ```bash
      #!/usr/bin/env bats
      
      generate_fixture() {
          local lines="$1"
          local file="$2"
      
          for i in $(seq 1 "$lines"); do
              echo "Line $i content" >> "$file"
          done
      }
      
      @test "Handle large input file" {
          generate_fixture 1000 "$TMPDIR/large.txt"
          run my_function "$TMPDIR/large.txt"
          [ "$status" -eq 0 ]
          [ "$(wc -l < "$TMPDIR/large.txt")" -eq 1000 ]
      }
      ```
      
      ## Advanced Patterns
      
      #
      
  • SKILL.md 5.1 KB
    ---
    name: bats-testing-patterns
    description: 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.
    ---
    
    # 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)
    
    ## Detailed patterns and worked examples
    
    Detailed pattern documentation lives in `references/details.md`. Read that file when the navigation tier above is insufficient.
    
    ## Testing Error Conditions
    
    ```bash
    #!/usr/bin/env bats
    
    @test "Function fails with missing file" {
        run my_function "/nonexistent/file.txt"
        [ "$status" -ne 0 ]
        [[ "$output" == *"not found"* ]]
    }
    
    @test "Function fails with invalid input" {
        run my_function ""
        [ "$status" -ne 0 ]
    }
    
    @test "Function fails with permission denied" {
        touch "$TMPDIR/readonly.txt"
        chmod 000 "$TMPDIR/readonly.txt"
        run my_function "$TMPDIR/readonly.txt"
        [ "$status" -ne 0 ]
        chmod 644 "$TMPDIR/readonly.txt"  # Cleanup
    }
    
    @test "Function provides helpful error message" {
        run my_function --invalid-option
        [ "$status" -ne 0 ]
        [[ "$output" == *"Usage:"* ]]
    }
    ```
    
    ### Testing with Dependencies
    
    ```bash
    #!/usr/bin/env bats
    
    setup() {
        # Check for required tools
        if ! command -v jq &>/dev/null; then
            skip "jq is not installed"
        fi
    
        export SCRIPT="${BATS_TEST_DIRNAME}/../bin/script.sh"
    }
    
    @test "JSON parsing works" {
        skip_if ! command -v jq &>/dev/null
        run my_json_parser '{"key": "value"}'
        [ "$status" -eq 0 ]
    }
    ```
    
    ### Testing Shell Compatibility
    
    ```bash
    #!/usr/bin/env bats
    
    @test "Script works in bash" {
        bash "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1
    }
    
    @test "Script works in sh (POSIX)" {
        sh "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1
    }
    
    @test "Script works in dash" {
        if command -v dash &>/dev/null; then
            dash "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1
        else
            skip "dash not installed"
        fi
    }
    ```
    
    ### Parallel Execution
    
    ```bash
    #!/usr/bin/env bats
    
    @test "Multiple independent operations" {
        run bash -c 'for i in {1..10}; do
            my_operation "$i" &
        done
        wait'
        [ "$status" -eq 0 ]
    }
    
    @test "Concurrent file operations" {
        for i in {1..5}; do
            my_function "$TMPDIR/file$i" &
        done
        wait
        [ -f "$TMPDIR/file1" ]
        [ -f "$TMPDIR/file5" ]
    }
    ```
    
    ## Test Helper Pattern
    
    ### test_helper.sh
    
    ```bash
    #!/usr/bin/env bash
    
    # Source script under test
    export SCRIPT_DIR="${BATS_TEST_DIRNAME%/*}/bin"
    
    # Common test utilities
    assert_file_exists() {
        if [ ! -f "$1" ]; then
            echo "Expected file to exist: $1"
            return 1
        fi
    }
    
    assert_file_equals() {
        local file="$1"
        local expected="$2"
    
        if [ ! -f "$file" ]; then
            echo "File does not exist: $file"
            return 1
        fi
    
        local actual=$(cat "$file")
        if [ "$actual" != "$expected" ]; then
            echo "File contents do not match"
            echo "Expected: $expected"
            echo "Actual: $actual"
            return 1
        fi
    }
    
    # Create temporary test directory
    setup_test_dir() {
        export TEST_DIR=$(mktemp -d)
    }
    
    cleanup_test_dir() {
        rm -rf "$TEST_DIR"
    }
    ```
    
    ## Integration with CI/CD
    
    ### GitHub Actions Workflow
    
    ```yaml
    name: Tests
    
    on: [push, pull_request]
    
    jobs:
      test:
        runs-on: ubuntu-latest
    
        steps:
          - uses: actions/checkout@v3
    
          - name: Install Bats
            run: |
              npm install --global bats
    
          - name: Run Tests
            run: |
              bats tests/*.bats
    
          - name: Run Tests with Tap Reporter
            run: |
              bats tests/*.bats --tap | tee test_output.tap
    ```
    
    ### Makefile Integration
    
    ```makefile
    .PHONY: test test-verbose test-tap
    
    test:
    	bats tests/*.bats
    
    test-verbose:
    	bats tests/*.bats --verbose
    
    test-tap:
    	bats tests/*.bats --tap
    
    test-parallel:
    	bats tests/*.bats --parallel 4
    
    coverage: test
    	# 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
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related