OpsInit: Testing Bash Scripts

Reading Time: 4 minutes

Shell scripting isn’t easy. A quick script like to perform mathematical calculations or string manipulation may seem like a walk in the park, but a problem arises when we try to use these scripts to build something big like an automation pipeline. Most of us happen to spend too much time on the command line and a good shell script is an incredibly powerful thing to drop into your existing workflow to expand its current capabilities.

In most languages, there’s a clearly agreed basic approach to testing, and most projects have at least a few tests written but it’s not the case with Bash scripts. Now, we’re going take a walkthrough of how Bats allows us to use verify our Bash scripts will behave under certain circumstances specified through “Test Cases”. Bash Automated Test System or “Bats” is a testing framework written for and in the Bash command language. The best thing about Bats is that it can be used for much more than testing your shell scripts! You can use it to test any UNIX program.


You can install Bats by running the following command:

sudo apt install bats

Also, we can check out a copy of the Bats repository and run the install.sh file.

Writing tests

At its simplest, a bats test might just be:

@test "Check that the docker is present" {
command -v docker

Breaking this down a little:

The @test “Check that the docker is present” { is how a test definition is declared. Spacing is important here. There must be a space after the description string and before the curly brace. The closing curly brace must be on a new line. Its output will be:


By convention, Bats tests are given the *.bats extension. This allows Bats to seek out tests in a directory and run a suite. There are several global variables you can use to look into Bats tests:

  • $BATS_TEST_FILENAME is the fully expanded path to the Bats test file.
  • $BATS_TEST_DIRNAME is the directory in which the Bats test file is located.
  • $BATS_TEST_NAMES is an array of function names for each test case.
  • $BATS_TEST_NAME is the name of the function containing the current test case.
  • $BATS_TEST_DESCRIPTION is the description of the current test case.
  • $BATS_TEST_NUMBER is the (1-based) index of the current test case in the test file.
  • $BATS_TMPDIR is the location to a directory that may be used to store temporary files.

Now, let’s look at the scenario where the test fails. Here, it’s checking for the file which doesn’t exist. Below is the snippet and output for the same.

@test "Check that we have a tmp file" {
run stat tmp
[ $status = 0 ]


setup and teardown

You can define special setup and teardown functions which basically is used to add pre-test and post-test statements. It runs before and after each test case, respectively. We can use these to load fixtures, set up your environment, and clean up when you’re done.

setup() {
echo "Inside setup()"

Specify Test Cases

teardown() {
echo "Inside teardown()"

skip: Skip test cases

Sometimes it so happens that our code is not completely ready while running a test case. So, we can skip a test you wish by using the skip command. The following snippet shows the usage of skip.

@test "Check that we have a tmp file" {
skip "Skipping This Test"
run stat tmp
[ $status = 0 ]

Here “$status” is reserved variable provided by Bats. The output will be generated as follows:


Testing Piped Commands

We can also use Bats to check the output of piped commands. The following example will illustrate this.

@test "Using Piped Commands" {
run echo 'My Name is Slim Shady' | cut -d' ' -f2
[ $output ="Name" ]

Here “$output” is reserved variable provided by Bats. The output of this will be generated as:


Code outside of test cases

You can include code in your test file outside of @test functions. For example, this may be useful if you want to check for dependencies and fail immediately if they’re not present. However, any output that you print in code outside of @test, setup or teardown functions must be redirected to stderr (>&2).

Bats Evaluation Process

In order to better understand what you can and can’t do outside of @test blocks and setup or teardown functions, it’s important to know how Bats processes test files.

First, each test file is preprocessed. The first run evaluates the file without running any test functions, counting all the test cases in the file. This essentially amounts to turning each @test block into a test function.

Then, each test file is executed n+1 times, where n is the number of test cases in the file. Each individual run again evaluates the entire test file, then invokes the setup function, if defined, then invokes the specified test function, and finally invokes teardown. An exit trap is responsible for printing the status.


Another excellent use case for Bats is integration testing because its only dependency is Bash, it makes for an excellent addition to a full CI workflow. There are lots of more specific techniques to look at here, from mocking to test helpers to test isolation practices, but this should be more than enough to get you started, so you can verify your shell scripts work. That’s all for now.


  1. Documentation: Bats
  2. Some real-world examples: Github Link
  3. Man Page of Bats


Written by 

Sudeep James Tirkey is a software consultant having more than 0.5 year of experience. He likes to explore new technologies and trends in the IT world. His hobbies include playing football and badminton, reading and he also loves travelling a lot. Sudeep is familiar with programming languages such as Java, Scala, C, C++ and he is currently working on DevOps and reactive technologies like Jenkins, DC/OS, Ansible, Scala, Java 8, Lagom and Kafka.