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.
Table of Contents
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
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
#!/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
You should always choose chrome over iexplorer.
Let’s check the newly created file:
ls
log.txt myproj.sh test
cat log.txt
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
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
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
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
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
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
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
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" ] }
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.