BATS lets you test your bash scripts with the rigor of other programming languages.

Here’s how a typical test looks:

#!/usr/bin/env bats

@test "simple greeting" {
  run echo "hello world"
  [ "$output" = "hello world" ]
}

@test "command not found" {
  run ls /nonexistent/directory
  [ "$status" -ne 0 ]
  [[ "$output" =~ "No such file or directory" ]]
}

When you run bats your_test_file.bats, it executes the code within each @test block. The run command executes the specified command in a subshell, capturing its exit status ($status) and standard output ($output). Assertions are then made against these captured values.

The fundamental problem BATS solves is the difficulty of reliably testing shell scripts. Unlike compiled languages, bash scripts are often developed interactively, with debugging via echo statements and manual execution. This approach quickly becomes unmanageable for anything beyond trivial scripts, leading to subtle bugs and brittle deployments. BATS introduces a structured, repeatable testing framework, allowing you to catch regressions and verify behavior systematically.

Internally, BATS works by sourcing your test files and then executing each function prefixed with @test. For each test, it creates a clean subshell environment. This isolation is crucial: changes made to environment variables or files within one test won’t affect others. The run command is the core mechanism for interacting with the script being tested. It executes the command and populates special BATS variables: $status (the exit code), $output (stdout), and $stderr (stderr). You then use standard bash conditional expressions ([ ] or [[ ]]) to assert that these captured values meet your expectations.

Let’s say you have a script myscript.sh that creates a temporary file and writes to it:

#!/bin/bash
TEMP_FILE=$(mktemp)
echo "data" > "$TEMP_FILE"
echo "$TEMP_FILE"

A BATS test for this might look like:

#!/usr/bin/env bats

load './bats-assert/load.bash' # Assuming bats-assert is loaded

@test "script creates temp file and writes data" {
  run ./myscript.sh
  local temp_file="$output" # The script outputs the temp file name

  assert_output "data" # Checks stdout
  assert_file_exist "$temp_file"
  assert_file_contain "$temp_file" "data"

  # Clean up the temporary file
  rm "$temp_file"
}

Here, load './bats-assert/load.bash' brings in helpful assertion functions. assert_output checks stdout, assert_file_exist verifies the file was created, and assert_file_contain checks its content. Crucially, the test cleans up the temporary file it created.

Beyond basic assertions, BATS supports setup and teardown functions. @setup runs before each test, and @teardown runs after each test. This is invaluable for setting up common preconditions (like creating directories or configuration files) and cleaning up afterward, ensuring tests are independent.

#!/usr/bin/env bats

@setup {
  mkdir -p /tmp/test_dir
  touch /tmp/test_dir/config.txt
}

@teardown {
  rm -rf /tmp/test_dir
}

@test "file exists in setup dir" {
  run ls /tmp/test_dir/config.txt
  [ "$status" -eq 0 ]
}

The most surprising thing about BATS is how effectively it bridges the gap between shell scripting and traditional software development testing paradigms, without introducing significant overhead or complexity. It doesn’t require learning a new DSL beyond standard bash; the "magic" is in the run command and the special variables it populates. You’re still writing bash, just within a testable structure. This allows developers to leverage familiar testing patterns—assertions, setup/teardown, test isolation—directly within their shell scripting workflow, dramatically improving script reliability and maintainability.

The standard assertion library for BATS is bats-assert. While you can use standard bash [ and [[ ]] for checks, bats-assert provides more expressive and readable functions like assert_equal, assert_failure, assert_success, assert_output, assert_file_exist, and assert_file_contain. To use it, you typically load it at the beginning of your test file.

The next hurdle is managing test dependencies and parallel execution.

Want structured learning?

Take the full Bash course →