Hero Image
- Philipp Ludewig

Bash Script Testing: A Journey to Bats-Core

Aloha people,

I found myself recently in the need to test some complex bash scripts. The problem? I had no idea how to test bash scripts. When I wrote bash scripts in the past, they were so small I was able to verify if they worked by just running them. I still wanted to do test driven development to save me some headache in the long run, so what should I do? I started out with what I would call native testing. Let's have a look.

#!/bin/bash

function ok () {
    declare -i retval="$2"

    if [[ ${retval} -eq 0 ]];
    then 
          echo -e "\e$(tput setaf 2)Test passed -> [${1}]\e$(tput setaf 7)"
          return 1
    else 
          echo -e "\e$(tput setaf 1)Test failed -> [${1}]\e$(tput setaf 7)"
          return 0
    fi
}

function someTest() {
if [[ $result == $expected ]];
then
   ok "text of test should it succeed" 0
else
   ok "text of test should it have failed" 1
fi
}

someTest && exit 1

exit 0

I am calling it native testing, as I wasn't using any testing framework yet. I had to use the “ok” function in each test file with long if statements for the assertions. This approach created this overhead where I had to manage the exit codes and return values of the functions. I had a few test files, so this overhead didn't help the test driven development process. Additionally, to that, these scripts needed a lot of environment variables that I needed to supply in a setup function otherwise something would break the process. This approach was helpful to get started, but at some point I had to use stubbing and to implement some functions for that wasn't possible for me considering the time constraints. I was longing for the possibility of stubbing. Long story short: After some quick research, I found a test framework to make this possible: Bats-Core. It made testing bash scripts a walk in the park.

Bats-Core

The Community seems to be around since 2011. The documentation is not up-to-date, but which documentation is ever, am I right? The core functionality is still under development and recent changes are going to be made for the grayhemp/bats-mock repository. Bats-core enabled me to use assertions similar to JUnit, to stub functions and to remove the overhead I had with the “native testing” approach.

I had my starting problems with Bats-Core, though. My colleagues and I are developing on different operational systems (Win/Linux) so I had to think about how I could make this available to everyone. The Bats-Core community has provided a docker image that you could use for your convenience. This wasn't possible for me, though, as I was working for a client that had not heard about Bats-Core before. Time was short on supply and for obvious security reasons I couldn't just use a non-client approved docker image. I had to build my own one. Luckily for me, alpine was allowed so I created the following image. (In the meantime I requested the client to approve all bats-core repos, but this took a while)

FROM alpine:3.18

WORKDIR /opt
RUN apk update \
&& apk add bash gawk git coreutils gcc make ncurses \
&& adduser bats-tester -D -s /bin/bash

WORKDIR /test_frameworks
RUN git init -q && \
    git submodule add https://github.com/bats-core/bats-core.git bats && \
    git submodule add https://github.com/bats-core/bats-support.git test_helper/bats-support && \
    git submodule add https://github.com/bats-core/bats-assert.git test_helper/bats-assert && \
    git submodule add https://github.com/bats-core/bats-file.git test_helper/bats-file && \
    git submodule add https://github.com/grayhemp/bats-mock.git test_helper/bats-mock

RUN cd /test_frameworks/bats && \
         ./install.sh /usr/local

RUN cd /test_frameworks/test_helper/bats-mock && \
         ./build install

WORKDIR /home/bats-tester
USER bats-tester
ENTRYPOINT ["/bin/bash"]

I added the packages coreutils, gcc, make for convenience and ncurses so bats-core could display the test results in a proper format with colours. Bats-core recommends to add its support repositories via git submodule but we could also just clone the repos to their respective folders.

I run the docker image now with an volume so I can write the code and test in the same shell.

docker run -it -v "$PWD:/<project-folder-name>" -w /path/to/folder/in/project/test <image id>

In the next post, I will write about the different repositories from the bats-core community and the two available options for stubbing from the users grayhemp and jasonkarns.