2020-04-01
Some people say that when you think you should add tests to your Bash script, it’s really a sign that you should switch to a “proper” language instead.
Well, since “Bash” is baked into the name of this thing, that’s clearly not an option, so hello, tests!
I’m aware of a few Bash testing frameworks, but the only one I’ve ever used is Bats; specifically, the actively maintained Bats-core fork of it. The original project has been dormant for over four years now, but Bats-core is alive and kicking.
I’ve written tests covering pretty much everything and run them in a GitHub Actions job, which was a bit annoying to set up, but I’ve learned a few things along the way.
Speaking of things I’ve learned: in writing more lines of test code than the script they are testing has, I have bumped into a few obstacles and learned a few new things while getting over them.
This one I knew already. It’s nice to have a tool that becomes
interactive when the user doesn’t supply all the required parameters,
but it’s messy to test. I could have made pbb prompt the user for a
title if they use pbb title
without another parameter, but
I didn’t; now it’s easy to test. I’ve always wanted to learn Expect, which would
allow me to test these things interactively, but until I do, I make them
non-interactive.
By default, Bats is silent and only prints output if a test fails, together with what exactly failed. For example, I’d check if a file contains a certain string with
grep -Fqx 'goatcountercode=mycode' .pbbconfig
If that fails, the output looks like
not ok 2 Set initial GoatCounter code
# (in test file gccode.bats, line 23)
# `grep -Fqx 'goatcountercode=mycode' .pbbconfig' failed
but that wouldn’t tell me what was in the file instead. If I just print the file contents first, that output will show up if the test fails:
cat .pbbconfig
grep -Fqx 'goatcountercode=mycode' .pbbconfig
gets me this test output:
not ok 2 Set initial GoatCounter code
# (in test file gccode.bats, line 24)
# `grep -Fqx 'goatcountercode=mycode' .pbbconfig' failed
<snip>
# blogtitle=Testblog
# goatcountercode=notmycode
A-ha! That makes it easier to track things down.
Want to check what files are there in case your existence check fails? Just print them first:
ls artifacts
[[ -f artifacts/index.html ]]
Bats recognizes two special functions, setup
and teardown
, which are run before and after each test.
For my tests, I used them to set up a fresh temp directory and
initialize all the things I need in there: a Git repo, helper files for
pbb, and so on. Bats provides a few variables
that come in handy to copy files from wherever the test files resides:
$BATS_TEST_DIRNAME
is the path to the directory containing
the test, for example.
These functions can be declared in a separate file and made available
to a test file using the load
directive; all of my test files just start with
load test_helper
and setup and teardown are taken care of.
Some of the pbb subcommands push to a remote Git repository. I was
surprised to find how easy it is to mock that: initialize a bare
repository in another directory and tell Git that that’s the remote! I
use this in my setup
function:
# Set up "remote" repo
local remote='/tmp/pbb-remote.git'
git init --quiet --bare "$remote"
# Set up local repo
local repo='/tmp/pbb-testdata'
mkdir -p "$repo"
git -C "$repo" init --quiet
# Tell local repo about remote
git -C "$repo" remote add origin "$remote"
And now I can push as my heart desires!
After diving into Actions a while ago and writing a few of my own, I obviously wanted to run my shiny new Bats tests automatically in a GitHub workflow as well.
There is an existing action to help set up Bats in a workflow, but it seemed pretty easy to do it directly. I ended up with this job:
test:
runs-on: 'ubuntu-latest'
steps:
- name: 'Check out code'
uses: 'actions/checkout@v2'
- name: 'Get Bats repository'
uses: 'actions/checkout@v2'
with:
repository: 'bats-core/bats-core'
ref: 'v1.1.0'
path: 'bats-core'
- name: 'Install Bats and dependencies, adjust PATH'
run: |
sudo apt-get install pandoc
cd bats-core
./install.sh "$GITHUB_WORKSPACE"
echo "::add-path::$GITHUB_WORKSPACE/bin"
echo "::add-path::$GITHUB_WORKSPACE"
- name: 'Run tests'
run: bats --tap test
This first checks out my code, and then the Bats code into a separate
directory. The third and most complex step installs Bats and then
adds it to the $PATH
. This eluded me for a long
time with cryptic error messages until I finally got it.
I also learned that if a Bats test fails with exit code 127, it’s
because a dependency is missing. To allow Bats to run pbb itself, I had
to add the $GITHUB_WORKSPACE
directory, which contains the
pbb
script, to the $PATH
, and even though pbb
doesn’t shy away from using whatever tool I like, all I need is
pre-installed on the GitHub-hosted
runners.
And finally, I had to use TAP-compliant output instead of the pretty colourized output I’m used to from interactive Bats usage; the runner doesn’t like the terminal escapes Bats uses and I didn’t feel like investigating. TAP-compliant output also says more clearly “CI”, so that’s fine.
These are the things I would have told my one month younger self:
setup
and teardown
functions$PATH
if you install it
yourself; exit status 127 means a dependency is missing