Bash和测试驱动开发

时间:2009-08-22 09:35:49

标签: bash tdd

当在bash中编写多个简单的脚本时,我常常想知道如何使代码可测试。

通常很难为bash代码编写测试,因为它在获取值并返回值的函数上很少,而在检查和设置环境中的某些方面的函数上很高,修改文件 - 系统,调用程序等 - 依赖于环境或具有副作用的功能。因此,设置和测试代码变得比他们测试的代码复杂得多。


例如,考虑一个简单的测试函数:

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

此功能的测试代码可能包括:

add_to_file.before:

foo
bar
baz

add_to_file.after:

bar
baz
foo
qux

测试代码:

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
}

这里有5行代码由6行测试代码和7行数据测试。


现在考虑一个稍微复杂的案例:

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

我甚至不能说如何开始为此编写测试...


那么,有没有一种很好的方法在bash脚本中做TDD,或者我应该放弃并将我的努力放在其他地方?

8 个答案:

答案 0 :(得分:34)

所以这就是我所学到的:

  1. 有些testing frameworks用bash和bash编写, 然而...

  2. 并不是说Bash不适合TDD(尽管有些 我想到的其他语言更合适,但是 Bash用于的典型任务(安装,系统 配置),很难编写和测试 特别难以设置测试。

  3. Bash中糟糕的数据结构支持使其难以分离 来自副作用的逻辑,实际上通常没有逻辑 在Bash脚本中。这使得很难将脚本分解为 可测试的块。有些功能可以测试,但是 这是例外,而不是规则。

  4. 功能是件好事(tm),但它们只能到目前为止。

  5. 嵌套功能可以更好,但它们也是有限的。

  6. 在一天结束时,通过重大努力可以实现一些覆盖 获得了,但它将测试代码中不太有趣的部分, 并将大部分测试作为好的(或坏的)旧手册 测试

  7. Meta:我决定回答(并接受)我自己的问题,因为我无法在Sinan Ünür's(已投票)和mouviciel's(已投票)答案中选择同样有用且富有洞察力的答案。我想注意Stefano Borini's回答,虽然最初没有给我留下深刻的印象,但我逐渐学会欣赏它。此外,他上面提到的design patterns or best practices for shell scripts answer(已投票)很有用。

答案 1 :(得分:11)

如果您正在使用测试同时编写代码,请尝试使用除参数之外不使用任何内容的函数,并且不要修改环境。也就是说,如果您的函数也可以在子shell中运行,那么它将很容易测试。它需要一些参数并输出到stdout或文件,或者它可能在系统上做某事,但调用者不会感觉到副作用。

是的,您将最终传递一些函数,这些函数传递一些可能是全局的WORKING_DIR变量,但与跟踪每个函数读取和修改的内容相比,这是一个小麻烦。启用单元测试也是免费的奖励。

尽量减少需要输出的情况。一个小的子shell滥用将很长时间保持良好的分离(以牺牲性能为代价)。

而不是调用函数的线性结构,设置一些环境,然后调用其他环境,几乎都在一个级别上,尝试使用最少的数据返回深度调用树。如果你从全球变量中采取自我禁欲,那么用bash返回东西是不方便的......

答案 2 :(得分:8)

从实施的角度来看,我建议shUnit

从实际的角度来看,我建议不要放弃。我在bash脚本上使用TDD,我确认这是值得的。

当然,我获得的测试行数是代码的两倍,但是使用复杂的脚本,测试工作是一项很好的投资。特别是当您的客户在项目结束时改变主意并修改某些要求时,情况就是如此。拥有回归测试套件对于更改复杂的bash代码非常有帮助。

答案 3 :(得分:5)

如果你编写一个足够大的bash程序来要求TDD,你使用的是错误的语言。

我建议你阅读我之前关于bash编程最佳实践的帖子,你可能会找到一些有用的东西来使你的bash程序可测试,但我的上述陈述仍然存在。

Design patterns or best practices for shell scripts

答案 4 :(得分:3)

撰写Meszaros称之为consumer tests的内容在任何语言中都很难。另一种方法是手动验证诸如rsync之类的命令的行为,然后编写单元测试以证明特定功能而无需访问网络。在这个稍微修改过的示例中,如果脚本使用关键字“test”运行,则使用$ run打印副作用

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"

答案 5 :(得分:3)

你可能想看看黄瓜/阿鲁巴。对我来说做得很好。

此外,您可以通过执行以下操作来存储您想要的所有内容:

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

答案 6 :(得分:0)

advanced bash scripting guide有一个断言函数的例子,但这里有一个更简单,更灵活的断言函数 - 只需使用$ *的eval来测试任何条件。

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和caller bash builtin是特定于bash shell的。

答案 7 :(得分:0)

看一下Outthentic框架 - 它旨在创建运行任何Bash代码的场景,然后使用正式的DSL分析stdout,构建任何Tdd / blackbox测试套件都很容易这个工具。