Should I test that?

Should I test shell scripts?

Verdict

Yes

Yes, give each shell script that CI, cron or a teammate runs one bats-core test that runs the whole script in a temporary folder with fake versions of the commands that reach other systems, and run ShellCheck on every script in CI.

Why

Yes, test shell scripts minimally: one bats-core test per script, plus ShellCheck in CI. The typical case is a bash script that CI, cron or a teammate runs to build, deploy or delete old files, so Blast radius is internal, because a failure stalls the team and no customer sees it, and Change frequency is rarely. Detectability is eventually, because bash carries on after a failed command unless the script sets set -e, and ignores a failure early in a pipeline without pipefail, so a half-done run often exits 0. Reversibility is with-effort, because half an artefact or a deleted folder needs a rerun and a repair. Test cost is moderate, because the script must take its paths as variables and the test needs fake commands on PATH, so rule R12 gives Test minimally.

When the decision changes
WhenDecisionWhy
The script deletes files on customers' machines, such as an installer or an updaterTest: run the script with each path variable empty and assert that it deletes nothingBlast radius rises to users and Reversibility to impossible, because deleted customer files have no backup; Detectability falls to same-day, because customers report the loss
The script is a CI step under set -euo pipefail that only builds code, and the next step fails without its outputDo not test the script; keep ShellCheck and set -euo pipefailDetectability falls to immediately and Reversibility to trivial, because a failure turns the build red and the script writes only to the CI workspace
A one-off script moves customer uploads on the production server to a new folder layoutTest it differently: run it on a copy of the uploads, compare file counts and checksums, and keep a backupChange frequency falls to once and Blast radius rises to users, because the script runs one time against files customers open
The script has grown into subcommands for the team's environments that change in most weeksTest: one bats-core test per subcommand, plus a regression test for every bugChange frequency rises to constantly, because each edit can break a subcommand nobody runs that week
A cron script calls a cloud CLI with the server's credentials, and the failure is an expired permission CI cannot reproduceTest it differently: the script pings a heartbeat monitor after each successful run, which alerts on a missing pingTest cost rises to heavy, because CI has no copy of the server's credentials, while Detectability stays eventually and Reversibility with-effort

What breaks if you don't test

A variable left empty because a folder moved turns rm -rf "$DIR/"* into a delete from the root folder. A backup step whose tar fails inside tar | gzip still uploads a truncated archive, because the pipeline returns the status of gzip, and the job exits 0. Nobody notices until someone needs a restore.

What you lose if you over-test

A test that fakes every command and asserts its exact arguments breaks when you reorder two cp lines and passes when files land in the wrong place. Tests that call the real aws or ssh need live credentials in CI. A script that needs dozens of tests is cheaper to test after a rewrite in Python, where functions take arguments and return values.

How to test

  1. Run ShellCheck on every .sh file in CI. It flags an unguarded rm -rf "$VAR/"* as SC2115.
  2. Start each bash script with set -euo pipefail and write every deleted path as ${VAR:?}, which stops the script when the variable is empty.
  3. Write one bats-core test per script: point its paths at $BATS_TEST_TMPDIR, put fake aws or ssh scripts first on PATH, and assert the exit status and the files left.
  4. Add a regression test for each bug that reaches a server.

When the answer changes

  • The script deletes files on machines other people own.
  • The script runs one time against production files.
  • The failures come from credentials or tools that CI does not have.

Real incident + Code example

The Steam update that deleted home folders

In January 2015 a Steam for Linux user reported that after he moved Steam's folder, starting Steam deleted every file his account owned, including a 3 TB backup drive under /media. The Register traced it to steam.sh, which ran rm -rf "$STEAMROOT/"* under the comment # Scary!. When STEAMROOT came out empty, the delete started at the root folder. ShellCheck now uses that line as its SC2115 example. Steam's case is the first row of the table, and the second test below fails on the Steam line:

#!/usr/bin/env bats
# clean.sh runs: rm -rf "${APP_ROOT:?}/"*

@test "empties the app folder" {
  mkdir "$BATS_TEST_TMPDIR/app" && touch "$BATS_TEST_TMPDIR/app/cache.bin"
  run env APP_ROOT="$BATS_TEST_TMPDIR/app" "$BATS_TEST_DIRNAME/clean.sh"
  [ "$status" -eq 0 ]
  [ ! -e "$BATS_TEST_TMPDIR/app/cache.bin" ]
}

@test "deletes nothing when APP_ROOT is empty" {
  # A fake rm first on PATH logs its arguments, so a missing guard deletes nothing here.
  mkdir "$BATS_TEST_TMPDIR/bin"
  printf '#!/bin/sh\necho "$*" >> "$RM_LOG"\n' > "$BATS_TEST_TMPDIR/bin/rm"
  chmod +x "$BATS_TEST_TMPDIR/bin/rm"
  run env RM_LOG="$BATS_TEST_TMPDIR/rm.log" PATH="$BATS_TEST_TMPDIR/bin:$PATH" \
    APP_ROOT="" "$BATS_TEST_DIRNAME/clean.sh"
  [ "$status" -ne 0 ]
  [ ! -e "$BATS_TEST_TMPDIR/rm.log" ]
}

FAQ

Should you unit test bash scripts?

Yes, give each bash script that CI or other people run one bats-core test that runs the whole script in a temporary folder. For most scripts the whole run is the unit.

How do I test a bash script?

Test a bash script with bats-core: point its paths at the test's temporary folder, put fake versions of aws or ssh first on PATH, and assert the exit status and the files it leaves.

Is ShellCheck enough to test shell scripts?

No, ShellCheck finds unsafe code such as an unguarded rm -rf "$VAR/"*, but it cannot tell whether a script copies the right files. Keep ShellCheck and add one bats-core test of a full run.

Should I rewrite a shell script in Python so I can test it?

Rewrite a shell script in Python when it parses data or branches on many options; keep bash when it mostly calls other programs in order. A Python function returns values, so its test needs no fake commands on PATH.