We’ve all been there: a small shell script that starts as a quick helper suddenly becomes a critical part of your workflow. Then one day, it breaks — maybe because of a missing argument, a typo, or an unexpected input. Debugging at 2 AM isn’t fun. That’s why adding tests to your shell scripts is a lifesaver. With Bats (Bash Automated Testing System), you can catch these issues early and make your scripts as reliable as any other piece of software. Let’s walk through how to set up Bats and test a simple script.
Installation
via brew
brew install bats-core
via apt
apt-get install bats
Project and Test Setup
Create directories
mkdir -p src
mkdir -p tests
src/greet.sh
cat << EOF > src/greet.sh
#!/usr/bin/env bash
set -euo pipefail
main() {
if [[ $# -eq 0 ]]; then
echo "Usage: $0 <name>" >&2
exit 1
fi
echo "Hello, $1!"
}
main "$@"
EOF
tests/test_greet.bats
cat << EOF > tests/test_greet.bats
#!/usr/bin/env bats
# Test successful run
@test "prints greeting with name" {
run ./src/greet.sh Alice
[ "$status" -eq 0 ]
[ "$output" = "Hello, Alice!" ]
}
# Test error case
@test "fails without arguments" {
run ./src/greet.sh
[ "$status" -ne 0 ]
[[ "$output" == *"Usage:"* ]]
}
EOF
Makefile
cat << EOF > Makefile
.PHONY: test lint all
lint:
shellcheck src/*.sh
test:
bats tests
all: lint test
EOF
Directory Structure
tree .
.
├── Makefile
├── src
│ └── greet.sh
└── tests
└── test_greet.bats
3 directories, 3 files
Running the Tests
Test run
make test
bats tests
test_greet.bats
✓ prints greeting with name
✓ fails without arguments
2 tests, 0 failures
Linting
Run the linter
make lint
shellcheck src/*.sh