<< All versions
Skill v1.0.1
currentAutomated scan100/100hermeticormus/libreuiux-claude-code/bats-testing-patterns
1 files
──Details
PublishedJune 6, 2026 at 02:45 AM
Content Hashsha256:bbf2ab75641bf998...
Git SHAe5a061ebeb85
Bump Typepatch
──Files
Files (1 file, 12.2 KB)
SKILL.md12.2 KBactive
SKILL.md · 632 lines · 12.2 KB
version: "1.0.1" 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)
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 Homebrewbrew install bats-core# Ubuntu/Debiangit clone https://github.com/bats-core/bats-core.gitcd bats-core./install.sh /usr/local# From npm (Node.js)npm install --global bats# Verify installationbats --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 presentload test_helper# Setup runs before each testsetup() {export TMPDIR=$(mktemp -d)}# Teardown runs after each testteardown() {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" = "line1line2line3" ]}@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 batssetup() {# Create test directoryTEST_DIR=$(mktemp -d)export TEST_DIR# Source script under testsource "${BATS_TEST_DIRNAME}/../bin/script.sh"}teardown() {# Clean up temporary directoryrm -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 batssetup() {# Create directory structuremkdir -p "$TMPDIR/data/input"mkdir -p "$TMPDIR/data/output"# Create test fixturesecho "line1" > "$TMPDIR/data/input/file1.txt"echo "line2" > "$TMPDIR/data/input/file2.txt"# Initialize environmentexport 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.shload test_helper# setup_file runs once before all testssetup_file() {export SHARED_RESOURCE=$(mktemp -d)echo "Expensive setup" > "$SHARED_RESOURCE/data.txt"}# teardown_file runs once after all teststeardown_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 commandmy_external_tool() {echo "mocked output"return 0}@test "Function uses mocked tool" {export -f my_external_toolrun my_function[[ "$output" == *"mocked output"* ]]}
Command Stubbing
bash
#!/usr/bin/env batssetup() {# Create stub directorySTUBS_DIR="$TMPDIR/stubs"mkdir -p "$STUBS_DIR"# Add to PATHexport PATH="$STUBS_DIR:$PATH"}create_stub() {local cmd="$1"local output="$2"local code="${3:-0}"cat > "$STUBS_DIR/$cmd" <<EOF#!/bin/bashecho "$output"exit $codeEOFchmod +x "$STUBS_DIR/$cmd"}@test "Function works with stubbed curl" {create_stub curl "{ \"status\": \"ok\" }" 0run 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_SETTINGrun 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 directorycp "$FIXTURES_DIR/input.txt" "$WORK_DIR/input.txt"# Run functionrun my_process_function "$WORK_DIR/input.txt"# Compare outputdiff "$WORK_DIR/output.txt" "$FIXTURES_DIR/expected_output.txt"}
Dynamic Fixture Generation
bash
#!/usr/bin/env batsgenerate_fixture() {local lines="$1"local file="$2"for i in $(seq 1 "$lines"); doecho "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
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 batssetup() {# Check for required toolsif ! command -v jq &>/dev/null; thenskip "jq is not installed"fiexport SCRIPT="${BATS_TEST_DIRNAME}/../bin/script.sh"}@test "JSON parsing works" {skip_if ! command -v jq &>/dev/nullrun 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; thendash "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1elseskip "dash not installed"fi}
Parallel Execution
bash
#!/usr/bin/env bats@test "Multiple independent operations" {run bash -c 'for i in {1..10}; domy_operation "$i" &donewait'[ "$status" -eq 0 ]}@test "Concurrent file operations" {for i in {1..5}; domy_function "$TMPDIR/file$i" &donewait[ -f "$TMPDIR/file1" ][ -f "$TMPDIR/file5" ]}
Test Helper Pattern
test_helper.sh
bash
#!/usr/bin/env bash# Source script under testexport SCRIPT_DIR="${BATS_TEST_DIRNAME%/*}/bin"# Common test utilitiesassert_file_exists() {if [ ! -f "$1" ]; thenecho "Expected file to exist: $1"return 1fi}assert_file_equals() {local file="$1"local expected="$2"if [ ! -f "$file" ]; thenecho "File does not exist: $file"return 1filocal actual=$(cat "$file")if [ "$actual" != "$expected" ]; thenecho "File contents do not match"echo "Expected: $expected"echo "Actual: $actual"return 1fi}# Create temporary test directorysetup_test_dir() {export TEST_DIR=$(mktemp -d)}cleanup_test_dir() {rm -rf "$TEST_DIR"}
Integration with CI/CD
GitHub Actions Workflow
yaml
name: Testson: [push, pull_request]jobs:test:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v3- name: Install Batsrun: |npm install --global bats- name: Run Testsrun: |bats tests/*.bats- name: Run Tests with Tap Reporterrun: |bats tests/*.bats --tap | tee test_output.tap
Makefile Integration
makefile
.PHONY: test test-verbose test-taptest:bats tests/*.batstest-verbose:bats tests/*.bats --verbosetest-tap:bats tests/*.bats --taptest-parallel:bats tests/*.bats --parallel 4coverage: test# Optional: Generate coverage reports
Best Practices
- Test one thing per test - Single responsibility principle
- Use descriptive test names - Clearly states what is being tested
- Clean up after tests - Always remove temporary files in teardown
- Test both success and failure paths - Don't just test happy path
- Mock external dependencies - Isolate unit under test
- Use fixtures for complex data - Makes tests more readable
- Run tests in CI/CD - Catch regressions early
- Test across shell dialects - Ensure portability
- Keep tests fast - Run in parallel when possible
- Document complex test setup - Explain unusual patterns
Resources
- Bats GitHub: https://github.com/bats-core/bats-core
- Bats Documentation: https://bats-core.readthedocs.io/
- TAP Protocol: https://testanything.org/
- Test-Driven Development: https://en.wikipedia.org/wiki/Test-driven_development