Bash脚本单元测试

162

我们有一个系统,除了Java代码外还有一些Bash脚本在运行。由于我们试图测试可能会出问题的所有内容,而这些Bash脚本可能会出问题,因此我们想要对它们进行测试。

问题是很难测试Bash脚本。

是否有一种方法或最佳实践来测试Bash脚本?还是应该放弃使用Bash脚本并寻找可测试的替代方案?


1
请参见:https://dev59.com/a3M_5IYBdhLWcg3wlEPO - Chen Levy
可能是Shell脚本的单元测试的重复问题。 - user
现有工具概述:https://medium.com/wemake-services/testing-bash-applications-85512e7fe2de - sobolevn
17个回答

58

3
我可以断言(说句双关语),截至目前为止,shunit2(版本2.1.6)有点出了问题。即使您直接提供值,assertNull和assertNotNull也无法正常工作。assertEquals工作正常,但我认为现在我只能自己动手编写了。 - labyrinth
@labyrinth,你确定问题不是这个吗:https://github.com/kward/shunit2/issues/53 "如何正确使用assertNull?"? - Victor Sergienko
1
@Victor,我可能没有在双引号上小心翼翼。我很快就要回到一个需要使用shunit2或其他bash单元测试系统的角色中。我会再试一次的。 - labyrinth
7
我是shunit2的用户和偶尔的贡献者,我可以证实该项目在2019年仍然活跃并且表现良好。 - Alex Harvey

39

TAP 兼容的Bash测试: Bash自动化测试系统

TAP,即测试任意协议,是测试工具中的测试模块之间的一种简单文本接口。TAP最初是Perl测试工具中的一部分,但现在还有C、C++、Python、PHP、Perl、Java、JavaScript等的实现。


16
值得披露TAP是什么以及为什么应该关注,否则就只是毫无意义的复制粘贴。 - om-nom-nom
1
@om-nom-nom:我现在已经将它链接到TAP网站了。 - Janus Troelsen
9
因为没有其他人说出不可说的话:TAP = 测试任何协议。 - JW.

32
我从讨论组得到了如下答案:

可以从外部文件中导入(包含、引入)一个过程(函数或其他名称)。这是编写测试脚本的关键:将脚本分解为独立的过程,然后可以将其导入到运行脚本和测试脚本中。这样,你的运行脚本就尽可能简单了。

这种方法类似于脚本的依赖注入,听起来很合理。避免使用Bash脚本并使用更易测试和不那么晦涩的语言是更可取的。

4
我不确定是应该点赞还是踩,一方面将其分解成较小的部分很好,但另一方面,我需要一个框架而不是一系列自定义脚本。 - mpapis
13
虽然bash没有问题(我写过很多脚本),但要精通它是很困难的。我的经验法则是,如果一个脚本足够大需要测试,那么你应该转向一种易于测试的脚本语言。 - Doug
1
但有时您需要在用户的 shell 中获取某些内容。我不清楚如果不使用 shell 脚本该怎么做。 - Itkovian
@Itkovian - 你可以使用npm将可执行文件导出到路径中,这样就不需要进行源代码引用了(你的npm包必须是全局安装的)。 - Eliran Malka
2
我会遵循不使用bash的建议。 :) - Maciej Wawrzyńczuk

16

我创建了 shellspec ,因为我想要一个易于使用且有用的工具。

它由纯 POSIX shell 脚本编写。它已经通过了比 shunit2 更多的 shell 测试。它比 bats/bats-core 有更强大的功能。

例如,它支持嵌套块、易于模拟/存根、易于跳过/挂起、参数化测试、断言行号、按行号执行、并行执行、随机执行、TAP/JUnit 格式化程序、覆盖率和CI 集成、分析器等。

请在项目页面上查看演示。


13

Nikita Sobolev 写了一篇优秀的博客文章,比较了几个不同的 Bash 测试框架,测试 Bash 应用程序。这些框架在该文章中列出:

对于急切的人:Nikita的结论是使用Bats,但似乎Nikita错过了Bats-core项目,这个项目似乎是未来要使用的,因为原始的Bats项目自2013年以来一直没有得到积极维护。


8
我不能相信没有人谈论OSHT!它与两个TAPJUnit兼容,它是纯shell(即没有涉及其他语言),它也可以独立工作,并且它简单而直接。
测试看起来像这样(摘自项目页面的片段):
#!/bin/bash
. osht.sh

# Optionally, indicate number of tests to safeguard against abnormal exits
PLAN 13

# Comparing stuff
IS $(whoami) != root
var="foobar"
IS "$var" =~ foo
ISNT "$var" == foo

# test(1)-based tests
OK -f /etc/passwd
NOK -w /etc/passwd

# Running stuff
# Check exit code
RUNS true
NRUNS false

# Check stdio/stdout/stderr
RUNS echo -e 'foo\nbar\nbaz'
GREP bar
OGREP bar
NEGREP . # verify empty

# diff output
DIFF <<EOF
foo
bar
baz
EOF

# TODO and SKIP
TODO RUNS false
SKIP test $(uname -s) == Darwin

一个简单的运行:
$ bash test.sh
1..13
ok 1 - IS $(whoami) != root
ok 2 - IS "$var" =~ foo
ok 3 - ISNT "$var" == foo
ok 4 - OK -f /etc/passwd
ok 5 - NOK -w /etc/passwd
ok 6 - RUNS true
ok 7 - NRUNS false
ok 8 - RUNS echo -e 'foo\nbar\nbaz'
ok 9 - GREP bar
ok 10 - OGREP bar
ok 11 - NEGREP . # verify empty
ok 12 - DIFF <<EOF
not ok 13 - TODO RUNS false # TODO Test Know to fail

最后一个测试结果显示为“不可行”,但退出代码为0,因为它是一个TODO。也可以设置详细模式:
$ OSHT_VERBOSE=1 bash test.sh # Or -v
1..13
# dcsobral \!= root
ok 1 - IS $(whoami) != root
# foobar =\~ foo
ok 2 - IS "$var" =~ foo
# \! foobar == foo
ok 3 - ISNT "$var" == foo
# test -f /etc/passwd
ok 4 - OK -f /etc/passwd
# test \! -w /etc/passwd
ok 5 - NOK -w /etc/passwd
# RUNNING: true
# STATUS: 0
# STDIO <<EOM
# EOM
ok 6 - RUNS true
# RUNNING: false
# STATUS: 1
# STDIO <<EOM
# EOM
ok 7 - NRUNS false
# RUNNING: echo -e foo\\nbar\\nbaz
# STATUS: 0
# STDIO <<EOM
# foo
# bar
# baz
# EOM
ok 8 - RUNS echo -e 'foo\nbar\nbaz'
# grep -q bar
ok 9 - GREP bar
# grep -q bar
ok 10 - OGREP bar
# \! grep -q .
ok 11 - NEGREP . # verify empty
ok 12 - DIFF <<EOF
# RUNNING: false
# STATUS: 1
# STDIO <<EOM
# EOM
not ok 13 - TODO RUNS false # TODO Test Know to fail

将其重命名为使用 .t 扩展名并将其放置在 t 子目录中,您可以使用 prove(1)(Perl 的一部分)来运行它:
$ prove
t/test.t .. ok
All tests successful.
Files=1, Tests=13,  0 wallclock secs ( 0.03 usr  0.01 sys +  0.11 cusr  0.16 csys =  0.31 CPU)
Result: PASS

设置OSHT_JUNIT或传递-j以生成JUnit输出。JUnit也可以与prove(1)结合使用。

我使用这个库来测试函数,通过引用它们的文件并使用IS/OK及其否定运行断言,以及使用RUN/NRUN来运行脚本。对我来说,这个框架提供了最少的开销,却获得了最大的收益。


7

Epoxy是一个Bash测试框架,我主要设计它来测试其他软件,但我也用它来测试Bash模块,包括它本身Carton

其主要优点是编码开销相对较低,无限的断言嵌套和灵活的选择来验证断言。

我制作了一个演示文稿,将其与BeakerLib进行比较 - 这是Red Hat中一些人使用的框架。


6

你为什么说测试Bash脚本很“难”?

下面这样的测试包装器有什么问题吗?

 #!/bin/bash
 set -e
 errors=0
 results=$($script_under_test $args<<ENDTSTDATA
 # inputs
 # go
 # here
 #
 ENDTSTDATA
 )
 [ "$?" -ne 0 ] || {
     echo "Test returned error code $?" 2>&1
     let errors+=1
     }

 echo "$results" | grep -q $expected1 || {
      echo "Test Failed.  Expected $expected1"
      let errors+=1
 }
 # And so on, et cetera, ad infinitum, ad nauseum
 [ "$errors" -gt 0 ] && {
      echo "There were $errors errors found"
      exit 1
 }

6
首先,Bash 脚本不是很易读。其次,期望值很复杂,例如检查是否创建了一个带有创建它的 Bash 脚本的 PID 的锁文件。 - nimcap
17
更重要的是,由于Shell脚本通常具有大量副作用并利用文件系统、网络等系统资源,所以很难对其进行测试。理想情况下,单元测试不会产生副作用,并且不依赖于系统资源。 - jayhendren

4

试试使用assert.sh

source "./assert.sh"

local expected actual
expected="Hello"
actual="World!"
assert_eq "$expected" "$actual" "not equivalent!"
# => x Hello == World :: not equivalent!

3

尝试使用bashtest。这是测试脚本的简单方法。例如,您有一个名为do-some-work.sh的脚本,它会更改一些配置文件。例如,在配置文件/etc/my.cfg中添加一行新的PASSWORD = 'XXXXX'

您可以逐行编写Bash命令,然后检查输出。

安装:

pip3 install bashtest

创建测试只是编写bash命令。
文件 test-do-some-work.bashtest:
# Run the script
$ ./do-some-work.sh > /dev/null

# Testing that the line "PASSWORD = 'XXXXX'" is in the file /etc/my.cfg
$ grep -Fxq "PASSWORD = 'XXXXX'" /etc/my.cfg && echo "YES"
YES

运行测试:
bashtest *.bashtest

您可以在这里这里找到一些示例。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接