Bash and Test-Driven Development

前端 未结 8 1537
礼貌的吻别
礼貌的吻别 2020-12-04 15:33

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 tha

相关标签:
8条回答
  • 2020-12-04 15:59

    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.

    0 讨论(0)
  • 2020-12-04 16:02

    You might want to take a look at cucumber/aruba. Did quite a nice job for me.

    Additionally, you can stub just about everything you want by doing something like this:

    #
    # code.sh
    #
    some_function_calling_some_external_binary()
    {
        if ! external_binary action_1; then
            # ...
        fi  
    
        if ! external_binary action_2; then
            # ...
        fi
    }
    
    #
    # test.sh
    #
    
    # now for the test, simply stub your external binary:
    external_binary()
    {
        if [ "$@" = "action_1" ]; then
            # stub action_1
    
        elif [ "$@" = "action_2" ]; then
            # stub action_2
    
        else
            external_binary $@
        fi  
    }
    
    0 讨论(0)
  • 2020-12-04 16:03

    If you are writing code at the same time with tests, try to make it high on functions that don't use anything besides their parameters and don't modify environment. That is, if your function might as well run in a subshell, then it will be easy to test. It takes some arguments and outputs something to stdout, or to a file, or maybe it does something on the system, but caller does not feel side effects.

    Yes, you will end up with big chain of functions passing down some WORKING_DIR variable that might as well be global, but this is minor inconvenience comparing to the task of tracking what does each function read and modify. Enabling unit tests is just a free bonus too.

    Try to minimize cases where you need output. A little subshell abuse will go long way to keeping things nicely separated (at the expense of performance).

    Instead of linear structure, where functions are called, set some environment, then other ones are called, all pretty much on one level, try to go for deep call tree with minimum data going back. Returning stuff in bash is inconvenient if you adopt self-imposed abstinence from global vars...

    0 讨论(0)
  • 2020-12-04 16:05

    Writing what Meszaros calls consumer tests is hard in any language. Another approach is to verify the behavior of commands such as rsync manually, then write unit tests to prove specific functionality without hitting the network. In this slightly-modified example, $run is used to print the side-effects if the script is run with the keyword "test"

    function distribute {
        local file=$1 ; shift
        for host in $@ ; do
            $run rsync -ae ssh $file $host:$file
        done
    }
    
    if [[ $1 == "test" ]]; then
        run="echo"
    else
        distribute schedule.txt $*
        exit 0
    fi
    
    #    
    # Built-in self-tests
    #
    
    output=$(mktemp)
    expected=$(mktemp)
    set -e
    trap "rm $got $expected" EXIT
    
    distribute schedule.txt login1 login2 > $output
    cat << EOF > $expected
    rsync -ae ssh schedule.txt login1:schedule.txt
    rsync -ae ssh schedule.txt login2:schedule.txt
    EOF
    diff $output $expected
    echo -n '.'
    
    echo; echo "PASS"
    
    0 讨论(0)
  • 2020-12-04 16:11

    The advanced bash scripting guide has an example of an assert function but here is a simpler and more flexible assert function - just use eval of $* to test any condition.

    assert() {
      if ! eval $* ; then
          echo
          echo "===== Assertion failed:  \"$*\" ====="
          echo "File \"$0\", line:$LINENO line:${BASH_LINENO[*]}"
          echo line:$(caller 0)
          exit 99
      fi  
    }
    
    # e.g. USAGE:
    assert [[ $r == 42 ]]
    assert "((r==42))"
    

    BASH_LINENO and caller bash builtin are bash shell specific.

    0 讨论(0)
  • 2020-12-04 16:12

    From an implementation point of view, I suggest shUnit2 or bats.

    From a practical point of view, I suggest not to give up. I use TDD on bash scripts and I confirm that it is worth the effort.

    Of course, I get about twice as many lines of test than of code but with complex scripts, efforts in testing are a good investment. This is true in particular when your client changes its mind near the end of the project and modifies some requirements. Having a regression test suite is a big aid in changing complex bash code.

    0 讨论(0)
提交回复
热议问题