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.

testing shell scripts cover

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