Testing Your Shell Scripts, with Bats
Because sometimes you want to know if they actually work.
Bash scripts are unloved and underappreciated. Many of us developers spend a lot of time on the command line, and a good shell script is an incredibly powerful thing to drop into & extend your existing workflow.
Shell scripting isn’t easy though. Many of the tools and techniques you might be used to aren’t nearly as effective or well-used on the command line. Testing is a good example: in most languages, there’s a clearly agreed basic approach to testing, and most projects have at least a few tests sprinkled around (though often not as many as they’d like).
In shell scripts though, testing is typically an afterthought. You’ll be hard pressed to find even the most popular shell scripts with any tests at all, and manual testing (or just not testing) is pretty standard. Scripts are often just quickly written, tested once for one use case, and considered ‘done’.
Sometimes quick scripting like that’s fine, but if you’re building anything substantial, it’s really not. I’ve recently released git-confirm and notes, both intended to be useful & reliable tools for others, and getting them to that stage without automated tests would’ve been drastically tougher (and catching issues in PRs would’ve been harder still).
Setting up a test environment
To start with, you’ll want to have a git repo set up with the basic outline of your project (a LICENSE, a file for your script, etc).
Bats is the core testing library, Bats-Assert adds lots of extra asserts for more readable tests, and Bats-Support adds some better output formatting (and is a prerequisite for Bats-Assert).
To pull all these submodules into test/libs, run the below from the root of your git repo and commit the result:
You’ll want a quick test file (addition-test.bats) to try this out:
This test has a few interesting steps:
- A shebang that loads the
batsexecutable relatively, from the libs submodule we’ve included, so that if you
chmod +x test/test.batsand run this file directly it’ll run itself with bats.
load 'file'— this is a Bats command to let you easily load files, here the
load.shscripts that initialise both Bats-Support and Bats-Assert.
@testcall, which defines a test, with a name and a body. Bats will run this test for us, and it passes if every single line within it returns a zero status code.
- A call to
assert_equal, checking that the output of
echo 1+1 | bcis 2.
You can run this with
That’s a bit of a mouthful though, so you’ll normally want a more convenient way to run these. I normally have two scripts, one (
test.sh) in the root of the project, which runs the tests a single time for a quick check and for CI, and one (
dev.sh) which watches my files and reruns the tests whenever they change, for quick development feedback.
chmod +x both of these, and you’re away. Contributors can now check out your project and run the tests with just:
If you want to run your tests in CI easily, I’d strongly recommend Travis CI, which will happily run these tests too, if you enable it for your repository and then just push a
.travis.yml file containing:
Writing your own tests
Once you have this set up, writing tests for Bats is pretty easy: work out what you’d run on the command line to check your program works, write exactly that wrapped with a
@test call and a name, and Bats will do the rest.
Checking the results of each step can be slightly tricky, since you can’t just eyeball command output, but the key is to remember that any failing line is a failing test. You can just write any bash conditional by itself on a line to define an assert, e.g.
[ -f "$file" ] ($file exists) or
[ -z "$result" ] ($result is empty).
You’ll also often want test setup and teardown steps to run before and after each of your tests, to manage their environment. Bats makes this incredibly easy: define functions called
teardown, and they’ll be automatically run before and after each test execution.
In many cases, you’ll want to run a command and assert on its resulting status and output, rather than immediately failing if it returns non-zero. Bats provides a
run function to make this easier, which wraps commands to return non-zero, and puts the command result into
$output variables. Bats-Assert then provides a nice selection of nice assertion functions to easily check these. Here’s an example from notes:
You can take a closer look at the real test in action here.
That’s all for now. There’s lots of more specific techniques to look at here, from mocking to test helpers to test isolation practices, but this should be more than enough to get you started, so you can verify your shell scripts work, and quickly tell whether future changes and PRs break anything. If you want more examples, take a look at the set of test included in notes and git-confirm, both of which cover some more interesting practices here.