---
title: 'Bash Script Testing: A Journey to Bats-Core'
url: 'https://blog.philippludewig.de/en/writing/lets-test-bash-scripts'
markdown: 'https://blog.philippludewig.de/en/writing/lets-test-bash-scripts.md'
lang: en
date: '2023-07-31'
description: '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…'
taxonomy:
  category:
    - blog
    - '2023'
    - tutorial
    - TDD
  tag:
    - bash
    - bats-core
---

# 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.

```bash
#!/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](https://bats-core.readthedocs.io/en/stable/) 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](https://github.com/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)

```Dockerfile
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.

```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.

<script id="diffblog-plugin-script" async="false" src="https://diff.blog/static/js/diffblog_plugin_v1.js"></script> 

<script> document.getElementById("diffblog-plugin-script").addEventListener("load", function () { DiffBlog( "rifghkd0g7ya9ovtjt66t9tw7himfb6dduwshidacju84nf7g3" ); }); </script> 

<script src="https://cdn.commento.io/js/commento.js"></script>

---

## Navigation

- Parent: [Life, Tech & Adventures: A Journey in Bytes and Boardgames](https://blog.philippludewig.de/en/index.md)
- Previous: [Let's assert and stub for bash scripts](https://blog.philippludewig.de/en/writing/lets-assert-and-stub-for-bash-scripts.md)
- Next: [Security Champion Day 2023](https://blog.philippludewig.de/en/writing/security-champion-day.md)
