当在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,或者我应该放弃并将我的努力放在其他地方?
答案 0 :(得分:34)
所以这就是我所学到的:
有些testing frameworks用bash和bash编写, 然而...
并不是说Bash不适合TDD(尽管有些 我想到的其他语言更合适,但是 Bash用于的典型任务(安装,系统 配置),很难编写和测试 特别难以设置测试。
Bash中糟糕的数据结构支持使其难以分离 来自副作用的逻辑,实际上通常没有逻辑 在Bash脚本中。这使得很难将脚本分解为 可测试的块。有些功能可以测试,但是 这是例外,而不是规则。
功能是件好事(tm),但它们只能到目前为止。
嵌套功能可以更好,但它们也是有限的。
在一天结束时,通过重大努力可以实现一些覆盖 获得了,但它将测试代码中不太有趣的部分, 并将大部分测试作为好的(或坏的)旧手册 测试
答案 1 :(得分:11)
如果您正在使用测试同时编写代码,请尝试使用除参数之外不使用任何内容的函数,并且不要修改环境。也就是说,如果您的函数也可以在子shell中运行,那么它将很容易测试。它需要一些参数并输出到stdout或文件,或者它可能在系统上做某事,但调用者不会感觉到副作用。
是的,您将最终传递一些函数,这些函数传递一些可能是全局的WORKING_DIR变量,但与跟踪每个函数读取和修改的内容相比,这是一个小麻烦。启用单元测试也是免费的奖励。
尽量减少需要输出的情况。一个小的子shell滥用将很长时间保持良好的分离(以牺牲性能为代价)。
而不是调用函数的线性结构,设置一些环境,然后调用其他环境,几乎都在一个级别上,尝试使用最少的数据返回深度调用树。如果你从全球变量中采取自我禁欲,那么用bash返回东西是不方便的......
答案 2 :(得分:8)
从实施的角度来看,我建议shUnit。
从实际的角度来看,我建议不要放弃。我在bash脚本上使用TDD,我确认这是值得的。
当然,我获得的测试行数是代码的两倍,但是使用复杂的脚本,测试工作是一项很好的投资。特别是当您的客户在项目结束时改变主意并修改某些要求时,情况就是如此。拥有回归测试套件对于更改复杂的bash代码非常有帮助。
答案 3 :(得分:5)
如果你编写一个足够大的bash程序来要求TDD,你使用的是错误的语言。
我建议你阅读我之前关于bash编程最佳实践的帖子,你可能会找到一些有用的东西来使你的bash程序可测试,但我的上述陈述仍然存在。
答案 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测试套件都很容易这个工具。