How To Start Testing Bash Scripts With BATS

icon of a gear with a checkmark inside it on a gray gradient

In this article we’ll learn how to use BATS to test bash scripts. Get familiarized with assertions, functions to clean up tests, and skipping tests.

What is BATS?

BATS or Bash Automated Testing System is a testing framework designed for Bash.

This automated testing tool is a process for validating if a bash program is functioning correctly and hitting the intended requirements before its release. As we are going to see in this article, BATS used for executing tests of bash scripts and returns the outcome of these tests. Any bash programmer can use BATS to benefit from: improved bug detection, increased reusability, enhanced reporting capabilities, faster check execution, and overall higher accuracy.

BATS offers the possibility for developers to use bash scripts/libraries and apply similar automated testing processes used by python or java developers.

How to Install BATS?

Suppose that you want to be starting your project in a git repository; we first go to our Terminal, and type the following to install “git” (if you don’t have it installed)

sudo apt update
sudo apt install git
git version
Output
git version 2.34.1

Create or navigate to your project directory and execute the following commands to set up your git repository, then install bats and its libraries by cloning the necessary repos as sub modules

git init
git submodule add https://github.com/bats-core/bats-core.git test/bats
git submodule add https://github.com/bats-core/bats-support.git test/test_helper/bats-support
git submodule add https://github.com/bats-core/bats-support.git test/test_helper/ bats-files
git submodule add https://github.com/bats-core/bats-assert.git test/test_helper/bats-assert

Setting up Your Bash Script

Let’s start by creating a new bash file with the following code:

nano myproj.sh
myproj.sh
#!/usr/bin/bash
choice="You should always choose $1 over $2."
echo "$choice";echo "$choice" > $3
exit 0

The preceding output displays that our bash scripts takes three arguments and printout the “choice” variable’s value in the terminal, additionally it create a text file contains the same output’s value. Let’s execute our program for better understanding.

sh myproj.sh chrome iexplorer log.txt
Output
You should always choose chrome over iexplorer.

Let’s check the newly created file:

ls
Output
log.txt myproj.sh test
cat log.txt
Output
You should always choose chrome over iexplorer.

Getting Started With Testing Bash Scripts

The best way to get familiar with BATS is to just start running simple tests. So let’s go ahead and create a new file named mytest.bats in the test directory with the following content:

@test "can run our script" {
    ./initproj.sh
}

To run the test, we should execute the following command from the root directory:

./test/bats/bin/bats test/mytest.bats
Output
mytest.bats
 ✗ can run our script
   (in test file test/mytest.bats, line 2)
     './project.sh' failed with status 127
   /home/ubuzz/my_project/test/mytest.bats: line 2: ./initproj.sh: No such file or directory

1 test, 1 failure

Obviously the test has failed because we don’t have such file name. So let’s create a simple bash script in src directory just as so:

mkdir src/
echo '#!/usr/bin/bash' > initproj.sh
chmod a+x initproj.sh

Run our previous test from the root directory by executing the below given command:

./test/bats/bin/bats test/mytest.bats
Output
mytest.bats
 ✓ can run our script

1 test, 0 failures

Now everything should be working fine.

How to Setup Your Tests

Setup functions are called before each individual test in the file. We can define only one setup function for all tests in that file.

Let’s add a setup function to our bats file, so that our test can execute initproj.sh directly without using a relative path. The bats file should look like this.

setup() {

    DIR="$( cd "$( dirname "$BATS_TEST_FILENAME" )" >/dev/null 2>&1 && pwd )"
    PATH="$DIR/../src:$PATH"
}


@test "can run our script" {
    initproj.sh
}

Note: After we added src to $PATH, we can now stop adding the previous used relative path src/project.sh.

Now that we have learned some simple BATS testing, it’s time test our bash program myproj.sh. Run the test by simply edit the mytest.bats file as follows:

cat test/mytest.bats
Output
setup() {
    DIR="$( cd "$( dirname "$BATS_TEST_FILENAME" )" >/dev/null 2>&1 && pwd )"
    PATH="$DIR/../src:$PATH"
}

@test "Should run pass arguments" {
    myproj.sh 'Assets' 'Liabilities' 'log.txt'
}

Note: Remember to move the bash program to the src directory  mv myproj.sh src.

-Notice that we passed the required arguments to our script

Run the test to check the results by executing the following command:

./test/bats/bin/bats test/mytest.bats
Output
mytest.bats
 ✓ can run our script

1 test, 0 failures

Look for the log.txt file in your root testing directory and display its contents to make sure that everything is working smoothly:

cat log.txt
Output
You should always choose Assets over Liabilities.

How To Use Asserts To Deal With Output

For this section will retarget our test to make sure that our bash program print out the intended string after we passed the arguments. To achieve that we should use bats-assert and its dependency bats-support by adding the load statements to the setup function; additionally we should use the assert_output helper to check for a match. Follow the below given script:

cat test/mytest.bats
Output
setup() {
    load 'test_helper/bats-support/load'
    load 'test_helper/bats-assert/load'

    DIR="$( cd "$( dirname "$BATS_TEST_FILENAME" )" >/dev/null 2>&1 && pwd )"
    PATH="$DIR/../src:$PATH"
}
@test "Should return message" { 

        a = 'Assets'
        l = 'Liabilities'
        t = 'log.txt'
    run myproj.sh "$a" "$l" "$t"
    assert_output "you should always choose $a over $l."
}

Note that we prefixed our project call with the run function (bats function) which is necessary when we use bats helpers.

Run the test to check the results.

./test/bats/bin/bats test/mytest.bats
Output
mytest.bats

 ✗ can run our script
   (from function 'assert_output' in file test/test_helper/bats-assert/src/assert_output.bash, line 194,
    in test file test/mytest.bats, line 15)
     'assert_output 'you should always choose $a over $l.'' failed 

   -- output differs --
   expected : you should always choose $a over $l.
   actual   : You should always choose $a over $l.
   --

1 test, 1 failure

You can notice the test has failed. BATS tells us that it didn’t get the expected output from our bash script. As you might have noticed, assert_output is case sensitive so we have to change its value to You should always choose $a over $l. Run the test again and everything should be ok.

In case you want to compare long strings it’s only logical to compare only parts ( substring) of the string. To achieve that we should use the --partial argument just as so:

@test "Should match substring" { 

        a = 'Assets'
        l = 'Liabilities'
        t = 'log.txt'
    run myproj.sh "$a" "$l" "$t"
    assert_output --partial "$a over $l."
}

We could also check multiple substrings if they have matches in the output string by adding multiple assert_output helpers. Check the following:

assert_output --partial "$a over $l."
assert_output --partial "You"

To test for negative matches, BATS provides us with refute_output helper. This is a way to compare if the helper’s value doesn’t match the bash program’s output.

@test " Should not match string" " { 

        a = 'Assets'
        l = 'Liabilities'
        t = 'log.txt'
    run myproj.sh "$a" "$l" "$t"
    refute_output "I always choose $a over $l."}

We can also use the --partial option with refute_output to test if the output doesn’t have the provided substring matches:

refute_output --partial '$a intead of $l.'

There is also the possibility to use refute_output --partial with assert_output --partial to test matching and none matching substring at the same time. Check the following .bats script:

setup() {
    load 'test_helper/bats-support/load'
    load 'test_helper/bats-assert/load'
    . . .
    . . .
}

@test "Should run the script" {
        a = 'Assets'
        l = 'Liabilities'
        t = 'log.txt'
    run myproj.sh "$a" "$l" "$t"
    assert_output --partial "$a over $l."
    refute_output --partial "you"
    refute_output --partial "instead of"

}

How To Deal With Files

In a previous section of this article we coded our bash script to save a file containing the output string within it; it is logical that we should edit our testing bats file so that it can test if our bash script is in fact creating a file after its execution.

@test "can run our script" {
        a='Assets'
        l='Liabilities'
        t='log.txt'
    run myproj.sh "$a" "$l" "$t"
    # Notice the backticks '
    dir_content='ls | grep $t'
    [ "$dir_content" == "$t" ]
}
Note: Linux command lines are enclosed in backquotes / backticks (‘)

After we have checked the existence of our text file, why not also check if that exact file has the correct string within it. Follow the below given script for better understanding:

. . .
    log_content='cat $t'
    [ "$log_content" == "You should always choose $a over $l." ]

}

How To Clean Up After a Test

As you have noticed, after each test a new file gets created. This could lead to some problems as we progress with our test suites. BATS offers us the possibility to implement a teardown function which can be used to handle cleaning up the mess generated by our tests. Check the following script:

Setup(){
. . .
}
teardown() {
    rm log.txt
}
    log_content='cat $t'
    [ "$log_content" == "You should always choose $a over $l." ]

}

How To Skip a Test

In some cases, it would be helpful to skip entire tests that are not fully functioning yet, or skip a test when a –if- condition is true. Fortunately BATS provides us with the skip command to do just that. Check the following:

@test "Should im.." {
        skip "Infrastructure not provided"

        a='Assets'
        l='Liabilities'
        t='log.txt'
    run myproj.sh "$a" "$l" "$t"
    dir_ =''
    [ "$dir_ " !=  ]
}

Note: No test statement after the skip command will be executed. You should keep in mind that if an error occurs before the skip command, the test will fail.

Conclusion

In this how to article, we’ve walked you through the different ways of testing bash scripts using different bash components. Additionally we created tests to get you acquainted with bats-asserts and assertion helpers.

We looked into setup and teardown functions as well as skipping tests. We hope these command may come in handy for you.

0 Shares:
Subscribe
Notify of
guest
Receive notifications when your comment receives a reply. (Optional)
Your username will link to your website. (Optional)

0 Comments
Inline Feedbacks
View all comments
You May Also Like