Bats Testing Patterns
Write bulletproof tests for shell scripts with Bats framework mastery
✨ The solution you've been looking for
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.
See It In Action
Interactive preview & real-world examples
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
Install
claude-code skill install bats-testing-patterns
claude-code skill install bats-testing-patternsConfig
First Trigger
@bats-testing-patterns helpCommands
| Command | Description | Required Args |
|---|---|---|
| @bats-testing-patterns test-driven-shell-script-development | Write comprehensive unit tests for shell utilities before and during development | None |
| @bats-testing-patterns ci/cd-pipeline-testing | Integrate Bats tests into automated build pipelines for shell-based deployment scripts | None |
| @bats-testing-patterns legacy-script-validation | Add comprehensive testing to existing shell scripts to prevent regressions during maintenance | None |
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
- 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
What Users Are Saying
Real feedback from the community
Environment Matrix
Dependencies
Framework Support
Context Window
Security & Privacy
Information
- Author
- wshobson
- Updated
- 2026-01-30
- Category
- automation-tools
Related Skills
Bats Testing Patterns
Master Bash Automated Testing System (Bats) for comprehensive shell script testing. Use when writing …
View Details →Bash Defensive Patterns
Master defensive Bash programming techniques for production-grade scripts. Use when writing robust …
View Details →Bash Defensive Patterns
Master defensive Bash programming techniques for production-grade scripts. Use when writing robust …
View Details →