bash-testing
Bash script testing with BATS (Bash Automated Testing System): test structure, assertions, setup/teardown, mocking external commands, CI integration, and coverage strategies.
Best use case
bash-testing is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Bash script testing with BATS (Bash Automated Testing System): test structure, assertions, setup/teardown, mocking external commands, CI integration, and coverage strategies.
Teams using bash-testing should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/bash-testing/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How bash-testing Compares
| Feature / Agent | bash-testing | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Bash script testing with BATS (Bash Automated Testing System): test structure, assertions, setup/teardown, mocking external commands, CI integration, and coverage strategies.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Bash Testing Skill
## When to Activate
- Writing tests for a new shell script
- Adding tests to an untested script
- Setting up BATS in a project
- Debugging why a BATS test is failing
- Mocking external commands in tests
- Adding BATS to a CI pipeline so shell scripts are tested alongside application code on every pull request
- Isolating a flaky integration test that depends on a real external command like `git`, `curl`, or `docker`
- Verifying every exit code path in a complex script that branches on multiple error conditions
- Writing unit tests for individual Bash functions by sourcing the script under test rather than executing it whole
---
## BATS Setup
### Installation
```bash
# npm (recommended for project-local install)
npm install --save-dev bats bats-support bats-assert
# Homebrew (macOS)
brew install bats-core
# Verify
bats --version
```
### Project structure
```
tests/
unit/ # function-level tests (source the script)
integration/ # full script execution tests
fixtures/ # static test data files
test_helper/
bats-support/ # helper library
bats-assert/ # assertion library
bats.config # optional BATS configuration
```
### package.json integration
```json
{
"scripts": {
"test:shell": "bats tests/ --recursive",
"test:shell:tap": "bats tests/ --recursive --tap"
}
}
```
---
## Test File Structure
```bash
#!/usr/bin/env bats
# Load helpers (adjust path to your project structure)
load 'test_helper/bats-support/load'
load 'test_helper/bats-assert/load'
# ── Setup / Teardown ──────────────────────────────────────────────────────────
setup() {
# Runs before EACH @test
TEST_TMP=$(mktemp -d)
# Point scripts to test fixtures
export CONFIG_DIR="$TEST_TMP"
}
teardown() {
# Runs after EACH @test — always runs even if test fails
rm -rf "$TEST_TMP"
}
setup_file() {
# Runs once before all tests in file
export TEST_SERVER_PID=$(start_test_server &)
}
teardown_file() {
# Runs once after all tests in file
kill "$TEST_SERVER_PID" 2>/dev/null || true
}
# ── Tests ─────────────────────────────────────────────────────────────────────
@test "exits 0 on valid input" {
run ./scripts/process.sh valid-input
assert_success
}
@test "exits 1 with missing argument" {
run ./scripts/process.sh
assert_failure
assert_output --partial "Usage:"
}
@test "outputs expected content" {
run ./scripts/generate.sh
assert_output "expected output"
}
@test "creates output file" {
run ./scripts/generate.sh "$TEST_TMP/result.txt"
assert_success
assert [ -f "$TEST_TMP/result.txt" ]
}
```
---
## Assertions (bats-assert)
```bash
assert_success # exit code 0
assert_failure # exit code != 0
assert_output "exact" # stdout equals exactly
assert_output --partial "part" # stdout contains substring
assert_output --regexp "^prefix" # stdout matches regex
refute_output # stdout is empty
assert_line "line content" # any line equals
assert_line --index 0 "first" # specific line equals
assert_line --partial "part" # any line contains
```
---
## Mocking External Commands
### PATH-based mocking (recommended)
Create a `test_helper/mock_bin/` directory with fake commands:
```bash
# tests/test_helper/mock_bin/curl
#!/usr/bin/env bash
echo '{"status":"ok"}'
exit 0
```
In your test:
```bash
setup() {
# Prepend mock bin to PATH
export PATH="$BATS_TEST_DIRNAME/test_helper/mock_bin:$PATH"
}
```
### Inline function override
```bash
@test "calls curl with correct URL" {
# Override curl for this test only
curl() {
echo "MOCK_CURL_CALLED: $*" >> "$TEST_TMP/calls.log"
echo '{"result":"mocked"}'
}
export -f curl
source ./scripts/fetch-data.sh
fetch_data "https://api.example.com/data"
assert [ -f "$TEST_TMP/calls.log" ]
run cat "$TEST_TMP/calls.log"
assert_output --partial "https://api.example.com/data"
}
```
### Mock with call recording
```bash
# tests/test_helper/mock_bin/git
#!/usr/bin/env bash
echo "$0 $*" >> "${MOCK_CALLS_LOG:-/tmp/mock_calls.log}"
case "$1" in
status) echo "nothing to commit" ;;
push) echo "Everything up-to-date" ;;
*) echo "mock git: $*" ;;
esac
```
---
## Unit Testing Functions (source mode)
Source the script to test individual functions without running `main`:
```bash
#!/usr/bin/env bats
load 'test_helper/bats-support/load'
load 'test_helper/bats-assert/load'
# Source the script — the BASH_SOURCE guard prevents main() from running
setup() {
source "$BATS_TEST_DIRNAME/../scripts/utils.sh"
}
@test "validate_path accepts relative paths" {
run validate_path "subdir/file.txt"
assert_success
}
@test "validate_path rejects .. traversal" {
run validate_path "../../../etc/passwd"
assert_failure
assert_output --partial "Invalid path"
}
@test "log_info writes to stderr" {
run log_info "test message"
assert_output --partial "[INFO]"
assert_output --partial "test message"
}
```
---
## Testing Exit Codes
```bash
@test "returns 0 on success" {
run ./scripts/process.sh good-input
assert_equal "$status" 0
}
@test "returns 2 on invalid argument" {
run ./scripts/process.sh --invalid-flag
assert_equal "$status" 2
}
```
---
## CI Integration
### GitHub Actions
```yaml
name: Shell Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install BATS
run: npm ci
- name: Run shell tests
run: npm run test:shell
- name: Lint scripts
run: |
sudo apt-get install -y shellcheck
shellcheck scripts/*.sh
```
---
## Checklist
- [ ] BATS and helpers installed (`bats-support`, `bats-assert`)
- [ ] Every function has at least one unit test (source mode)
- [ ] Every exit code path tested
- [ ] External commands mocked via PATH or function override
- [ ] `setup()` creates isolated temp dir, `teardown()` removes it
- [ ] Tests pass with `set -euo pipefail` active
- [ ] CI runs both BATS and shellcheckRelated Skills
visual-testing
Visual Regression Testing: tool comparison (Chromatic/Percy/Playwright screenshots/BackstopJS), pixel-diff vs AI-based comparison, baseline management, flakiness strategies (masks, tolerances, waitForLoadState), CI integration with GitHub Actions, and Storybook integration.
typescript-testing
TypeScript testing patterns: Vitest for unit/integration, Playwright for E2E, MSW for API mocking, Testing Library for React components. Core TDD methodology for TypeScript/JavaScript projects.
swift-testing
Swift testing patterns: Swift Testing framework (Swift 6+), XCTest for UI tests, async/await test cases, actor testing, Combine testing, and XCUITest for UI automation. TDD for Swift/SwiftUI.
swift-protocol-di-testing
Protocol-based dependency injection for testable Swift code — mock file system, network, and external APIs using focused protocols and Swift Testing.
scala-testing
Scala testing with ScalaTest, MUnit, and ScalaCheck: FunSpec/FlatSpec test structure, property-based testing with forAll, mocking with MockitoSugar, Cats Effect testing with munit-cats-effect (runTest/IOSuite), ZIO Test, Testcontainers-Scala for database integration tests, and CI integration with sbt. Use when writing or reviewing Scala tests.
rust-testing
Rust testing patterns — unit tests with mockall, integration tests with sqlx transactions, HTTP handler testing (axum), benchmarks (criterion), property tests (proptest), fuzzing, and CI with cargo-nextest.
rust-testing-advanced
Advanced Rust testing anti-patterns and corrections — cfg(test) placement, expect() over unwrap(), mockall expectation ordering, executor mixing (#[tokio::test] vs block_on), PgPool isolation with
ruby-testing
RSpec testing patterns for Ruby and Rails — factories, mocks, request specs, feature specs, VCR, and SimpleCov coverage.
r-testing
R testing patterns: testthat 3e with expect_* assertions, snapshot testing, mocking with mockery and httptest2, covr code coverage, lintr static analysis, property-based testing with hedgehog, testing Shiny apps with shinytest2. Use when writing or reviewing R tests.
python-testing
Python testing strategies using pytest, TDD methodology, fixtures, mocking, and parametrization. Core testing fundamentals.
python-testing-advanced
Advanced Python testing — async testing with pytest-asyncio, exception/side-effect testing, test organization, common patterns (API, database, class methods), pytest configuration, and CLI reference. Extends python-testing.
php-testing
PHP testing patterns: PHPUnit 11 with mocks and data providers, Pest v3 with expectations and datasets, Laravel feature/HTTP tests with RefreshDatabase, Symfony WebTestCase, PHPStan static analysis, Infection mutation testing. Use when writing or reviewing PHP tests.