1. Introduction

Bash is an immensely useful and popular scripting language that has a huge number of possible use-cases. Despite the fact that the language itself is very pervasive, the idea of testing it is not as prevalent as in languages like Java. This can lead to costly errors and less confidence in the code.

In this tutorial, we’ll learn how to change this state of affairs and use the Bats framework to test our Bash scripts.

2. Setup

2.1. Installing Bats

Firstly, we need to install Bats. It can be done in a few ways, but we’ll choose the most straightforward one. We’ll simply clone required repositories from GitHub to our project as submodules:

$ 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-assert.git test/test_helper/bats-assert
$ git submodule add https://github.com/bats-core/bats-assert.git test/test_helper/bats-files

2.2. Creating Project to Test

Secondly, we need something to test. So let’s create a simple script that will concatenate strings:

#!/usr/bin/env bash

concatenated="$1$2"
echo "$concatenated"
echo "$concatenated" > $3

exit 0

It’ll print a new string to the console but also will save it to a file. The file will be named accordingly to the third parameter of the script. Let’s call this little program project.sh and place it in the src directory.

2.3. Configuring a Test Suit

Finally, we need to create and set up the test file. Let’s create the test directory and place an empty test.bats file inside it. Before writing any actual tests, we need to load all the Bats helpers we want to use. Additionally, we’ll add our src directory to the path variable to simplify writing tests:

#!/usr/bin/env bash

setup() {
    load 'test_helper/bats-support/load'
    load 'test_helper/bats-assert/load'
    load 'test_helper/bats-file/load'
    DIR="$( cd "$( dirname "$BATS_TEST_FILENAME" )" >/dev/null 2>&1 && pwd )"
    PATH="$DIR/../src:$PATH"
}

3. Simple Test

The most basic test we can write will check if our script will run without an error. For the moment we ignore the output of the script, we just want to know if it was completed:

@test "should run script" {
    project.sh 'Hello ' 'Baeldung' '/tmp/output'
}

To run the test, we need to invoke the Bats library from the cloned repository:

$ ./test/bats/bin/bats test/test.bats
 ✓ should run script

1 tests, 0 failures, 0 skipped

4. Asserting the Output of the Script

Because we loaded the bats-assert component in the setup method, we can quickly write assertions on the output string using provided helpers. To check if the output matches the provided string exactly, we need to use the assert_output helper:

@test "should return concatenated strings" {
    run project.sh 'Hello ' 'Baeldung' '/tmp/output'

    assert_output 'Hello Baeldung'
}

In many cases, the output is long and complicated, so comparing it one to one isn’t practical. Fortunately, we can check if only part of the string matches:

assert_output --partial 'Baeldung'

Finally, we can also check if the output doesn’t match the provided string:

refute_output 'Hello World'

5. Asserting Files

5.1. Asserting Existence

Our script saves a concatenated string to a file, and we’d like to test that feature. Thankfully, the bats-file component provides us with convenient helpers to assert the existence of a file. Let’s write a test that checks if output file exists:

@test "should create file" {
    run project.sh 'Hello ' 'Baeldung' '/tmp/output'

    assert_exist /tmp/output
}

5.2. Cleaning Up

Every time we execute our script, it creates an output file. This can create various problems ranging from creating a mess on the disk to making the test non-deterministic. Fortunately, there is a simple solution: we should delete all temporary files after each test. However, we don’t need to implement that ourselves in every test. Instead, we can use a special teardown function:

teardown() {
    rm -f /tmp/output
}

It’ll be executed automatically by Bats after each test. If we’d like to execute it only once after all tests, we can also use the teardown_file function.

6. Other Assertions

Assertion helpers are very convenient but also limited in their capabilities. If we need to move beyond them, we can assert any boolean expression written in Bash. For example, we have already asserted that the output file exists, but we didn’t check if it contained correct content. Let’s do that by loading it to the variable with the cat command and checking it using the conditional expression:

@test "should write to file" {
    run project.sh 'Hello ' 'Baeldung' '/tmp/output'

    file_content=`cat /tmp/output`
    [ "$file_content" == 'Hello Baeldung' ]
}

7. Skipping Tests

Sometimes we need to exclude a test. Maybe we wrote a test for a feature that is not implemented yet, or there’s a bug that we would like to ignore for the moment. We could comment it out, but that creates the possibility of simply forgetting about it and isn’t very elegant. Instead, we’ll use the skip helper to mark a test to be ignored:

@test "should write logs" {
    skip "Logs are not implemented yet"
    run project.sh 'Hello ' 'Baeldung' '/tmp/output'

    file_content=`cat /tmp/logs`
    [ "$file_content" == 'I logged something' ]
}

Now, if we run the tests, we’ll get information about the skipped test:

$ ./test/bats/bin/bats test/test.bats
 ✓ should run script
 ✓ should return concatenated strings
 ✓ should create file
 ✓ should write to file
 - should write logs (skipped: Logs are not implemented yet)

5 tests, 0 failures, 1 skipped

8. Test Anything

Because Bash is so often used to execute other programs and operate on results they produced, Bats can be effectively used to test anything. Let’s say our project needs to have the node installed in version 12. We can check it by executing node with –version flag and then asserting the output:

@test "test node version" {
    run node --version

    assert_output --partial "v12"
}

The test will fail if the returned string doesn’t contain “v12” but also when node isn’t installed at all.

9. Summary

In this tutorial, we learned how to use the Bats library for testing Bash scripts. We created basic tests, used assertion helpers provided by the library, and learned how to make our own assertions. We investigated how to work with files and how to clean up after tests. Finally, we looked into the possibility of testing more than just Bash scripts.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.