Bash and Test-Driven Development

bashtdd

When writing more than a trivial script in bash, I often wonder how to make the code testable.

It is typically hard to write tests for bash code, due to the fact that it is low on functions that take a value and return a value, and high on functions that check and set some aspect in the environment, modify the file-system, invoke a program, etc. – functions that depend on the environment or have side effects. Thus the setup and test code become much more complicated than the code they test.


For example, consider a simple function to test:

function add_to_file() {
  local f=$1
  cat >> $f
  sort -u $f -o $f
}

Test code for this function might consist of:

add_to_file.before:

foo
bar
baz

add_to_file.after:

bar
baz
foo
qux

And test code:

function test_add_to_file() {
   cp add_to_file.{before,tmp}
   add_to_file add_to_file.tmp
   cmp add_to_file.{tmp,after} && echo pass || echo fail
   rm add_to_file.tmp
}

Here 5 lines of code are tested by 6 lines of test code and 7 lines of data.


Now consider a slightly more complicated case:

function distribute() {
   local file=$1 ; shift
   local hosts=( "$@" )
   for host in "${hosts[@]}" ; do
     rsync -ae ssh $file $host:$file
   done
}

I can't even say how to start write a test for that…


So, is there a good way to do TDD in bash scripts, or should I give up and put my efforts elsewhere?

Best Answer

So here is what I learned:

  1. There are some testing frameworks written in bash and for bash, however...

  2. It is not so much that Bash is not suitable for TDD (although some other languages come to mind that are a better fit), but the typical tasks that Bash is used for (Installation, System configuration), that are hard to write tests for, and in particularly hard to setup the test.

  3. The poor data structure support in Bash makes it hard to separate logic from side-effect, and indeed there is typically little logic in Bash scripts. That makes it hard to break scripts into testable chunks. There are some functions that can be tested, but that is the exception, not the rule.

  4. Function are a good thing (tm), but they can only go so far.

  5. Nested functions can be even better, but they are also limited.

  6. At the end of the day, with major effort some coverage can be obtained, but it will test the less interesting part of the code, and will keep the bulk of the testing as a good (or bad) old manual testing.

Meta: I decided to answer (and accept) my own question, because I was unable to choose between Sinan Ünür's (voted up) and mouviciel's (voted up) answers that where equally useful and insightful. I want to note Stefano Borini's answer, that although not impressed me initially, I learned to appreciate it over time. Also his design patterns or best practices for shell scripts answer (voted up) referred above was useful.