DEV Community

gasparev
gasparev

Posted on • Originally published at gasparevitta.com

How to test Bash scripts (2020 guide)

How do you test your bash scripts? Do you test them at all?

Bash scripting is one of the handiest tools every developer has to automate tasks, ease the everyday activities, help to reduce the toil. But automation tools can actually increase the toil if are not treated the same way as code. Bash scripts can get out of date and stop working, became hard to maintain, and increase your technical debt. You need to check them in version control, write tests, and use CI.

In this article, we'll go through how you can improve the quality of your automation scripts writing tests for Bash.

Bach

Bach is a testing framework for Bash that provides the possibility to write unit tests for your Bash scripts. Bach makes any command in the PATH an external dependency of the tested Bash script so that no command will be actually executed but will run as "dry run". In this way, you will be able to test the logic of the script and not the commands themself. Bach mocks all commands by default and provides a set of APIs for executing real commands if necessary.

How to install Bach

Prerequisites

To install Bach Testing Framework download bach.sh to your project, use the source command to import bach.sh.

For example:

source path/to/bach.sh
Enter fullscreen mode Exit fullscreen mode

What can be done

In Bach, we can test what the Bash script will actually execute.

Every test case in Bach is made of two functions: one for running tests and the other for asserting.

When you run your tests Bach will execute the two functions separately and will compare the sequence of commands executed by both functions. Every testing function must start with the name test-, the asserting function must end with -assert.

How to write test cases

Let's see some practical examples of how to write test cases.

#!/usr/bin/env bash
set -euo pipefail
source bach.sh
Enter fullscreen mode Exit fullscreen mode

To enable the Bach Testing Framework the first thing to do is to source the bash.sh.

test-rm-your-dot-git() {
    @mock find ~ -type d -name .git === @stdout ~/src/your-awesome-project/.git \
                                                ~/src/code/.git
    find ~ -type d -name .git | xargs -- rm -rf
}
test-rm-your-dot-git-assert() {
    rm -rf ~/src/your-awesome-project/.git ~/src/code/.git
}
Enter fullscreen mode Exit fullscreen mode

The first script we test is a command to find all the .git folders and remove them. We can test it by mocking the find command with those parameters to output two directories.

test-mock-script-with-custom-complex-action() {
    @mock ./path/to/script <<\SCRIPT
if [[ "$1" == foo ]]; then
  @echo bar
else
  @echo anything
fi
SCRIPT
    ./path/to/script foo
    ./path/to/script something
}
test-mock-script-with-custom-complex-action-assert() {
    bar
    anything
}
Enter fullscreen mode Exit fullscreen mode

In the second script, we use Bach to test a script that returns a different output based on the input. In this case, we mock the script using @mock ./path/to/script and then we define the script behavior.

test-bach-framework-mock-commands() {
    @mock find . -name fn === @stdout file1 file2

    ls $(find . -name fn)

    @mock ls file1 file2 === @stdout file2 file1

    ls $(find . -name fn) | xargs -n1 -- do-something

    @mock ls === @stdout foo bar foobar
    ls | xargs -n2 -- bash -c 'do-something ${@}' -s
}
test-bach-framework-mock-commands-assert() {
    ls file1 file2

    do-something file2
    do-something file1

    bash -c 'do-something ${@}' -s foo bar
    bash -c 'do-something ${@}' -s foobar
}
Enter fullscreen mode Exit fullscreen mode

In this script, you can see how to perform some complex operations like mocking a command and execute it in a $(...) expression and using pipes.

test-bach-framework-set--e-should-work() {
    set -e

    do-this
    builtin false

    should-not-do-this

}
test-bach-framework-set--e-should-work-assert() {
    do-this
    @fail
}
Enter fullscreen mode Exit fullscreen mode

Here we test the behavior of set -e so we make the script fail with builtin false and we test the failure using @fail.

test-no-double-quote-star() {
    @touch bar1 bar2 bar3 "bar*"

    function cleanup() {
        rm -rf $1
    }

    # We want to remove the file "bar*", not the others
    cleanup "bar*"
}
test-no-double-quote-star-assert() {
    # Without double quotes, all bar files are removed!
    rm -rf "bar*" bar1 bar2 bar3
}

test-double-quote-star() {
    @touch bar1 bar2 bar3 "bar*"

    function cleanup() {
        rm -rf "$1"
    }

    # We want to remove the file "bar*", not the others
    cleanup "bar*"
}
test-double-quote-star-assert() {
    # Yes, with double quotes, only the file "bar*" is removed
    rm -rf "bar*"
}
Enter fullscreen mode Exit fullscreen mode

Here we define a function that takes an argument and removes it. In the first example, we don't use double quotes in the function, this will remove all the files starting with bar-. In the second example, the function is using double quotes so only the file called bar* is removed.

How to run Bach tests

You can write all your test cases in a single .sh file remembering to add the source bach.sh in it. At this point to run the tests you just need to execute the .sh file.

This is it!

In this post, we've seen how to write unit tests for your Bash scripts to improve the quality and reliability of your automation.

Reach me on Twitter @gasparevitta and let me know your thoughts!

This article was originally published on my blog. Head over t​h​e​r​e if you like this post and want to read others like it!

Top comments (0)