单元测试bash脚本


112

我们有一个除了Java代码外还运行一些bash脚本的系统。由于我们正在尝试测试可能会破坏的所有内容,而那些bash脚本可能会破坏,因此我们想对其进行测试。

问题是很难测试bash脚本。

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




现有的工具概述:medium.com/wemake-services/...
sobolevn

Answers:


48

实际上,有一个shunit2,这是一个基于xUnit的单元测试框架,用于基于Bourne的shell脚本。我自己没有使用过,但是可能值得一试。

之前曾问过类似的问题:


3
我可以断言(双关语意)shunit2(2.1.6版)到目前为止已损坏。assertNull和assertNotNull不起作用,即使您将它们直接喂入值也是如此。assertEquals可以正常工作,但是我认为我现在只需要自己动手。
迷宫

@labyrinth,您确定问题不是这种情况吗:github.com/kward/shunit2/issues/53 “如何正确使用assertNull?”?
维克托·谢尔吉安科

1
@Victor肯定有可能我对双引号的注意不够。我很快又回到一个角色,其中shunit2或某些bash单元测试系统将非常有用。我会再试一次。
迷宫

5
我是shunit2的用户,有时甚至是该文件的贡献者,我可以确认该项目在2019
Alex Harvey

31

我从一个讨论组得到以下答案:

可以从外部文件导入(包括任何内容)过程(函数,无论其名称如何)。这就是编写测试脚本的关键:将脚本分解成独立的过程,然后将其导入运行脚本和测试脚本中,然后使运行脚本尽可能简单。

此方法类似于脚本的依赖注入,听起来很合理。最好避免使用bash脚本,而使用更具可测试性和不太模糊的语言。


4
我不确定是否应该投票赞成或反对,一方面划分为较小部分是好的,但是另一方面,我需要一个框架而不是一组自定义脚本
mpapis 2011年

10
虽然bash没什么问题(我写了很多脚本),但这是一门很难掌握的语言。我的经验法则是,如果脚本足够大以至于需要测试,那么您可能应该继续使用易于测试的脚本语言。
道格

1
但是有时候您需要拥有可以从用户的外壳中获取的东西。对我来说,目前尚不清楚您如何不借助shell脚本来做到这一点
Itkovian 2015年

@Itkovian-例如,您可以使用npm将可执行文件导出到路径,因此无需采购(必须在全球范围内安装npm软件包)
Eliran Malka

1
我将遵循有关不使用bash的建议。:)
MaciejWawrzyńczuk18年

30

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

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


14
值得介绍什么是TAP以及为什么要小心,否则它只是毫无意义的复制粘贴
om-nom-nom 2014年

@ om-nom-nom:我现在将其链接到TAP站点。
Janus Troelsen,2014年

7
既然没有人说这难以理解:TAP = Test Anything Protocol
JW。

9

Nikita Sobolev写了一篇很棒的博客文章,比较了几种不同的bash测试框架:测试Bash应用程序

对于不耐烦的人:Nikita的结论是使用Bats,但是Nikita似乎错过了Bats-core项目,在我看来这是一个可以继续使用的项目,因为自2013年以来一直没有积极维护原始的Bats项目。


7

Epoxy是我主要用于测试其他软件的Bash测试框架,但是我也使用它来测试bash模块,包括本身Carton

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

我做了一个演示,将其与BeakerLib进行了比较,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
 }

4
首先,bash脚本不是很可读。其次,期望很复杂,例如检查是否使用创建锁文件的bash脚本的PID创建了锁文件。
nimcap

10
更重要的是,很难测试shell脚本,因为它们通常具有大量的副作用,并且会利用文件系统,网络等系统资源。理想情况下,单元测试是无副作用的,并且不依赖于系统资源。
jayhendren

4

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

它由纯POSIX shell脚本编写。它比shunit2对许多外壳进行了更多测试。它具有比bats / bats-core强大的功能。

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

请参阅项目页面上的演示。


3

我非常喜欢shell2junit,该实用程序可从Bash脚本测试生成类似JUnit的输出。这很有用,因为生成的报告随后可以被持续集成系统(例如Jenkins和Bamboo的JUnit插件)读取。

尽管shell2junit没有提供像shunit2这样的全面的Bash脚本框架,但它确实允许您很好地报告测试结果。


3

尝试bashtest。这是测试脚本的简单方法。例如,您do-some-work.sh更改了一些配置文件。例如,将新行添加PASSWORD = 'XXXXX'到配置文件/etc/my.cfg

您逐行编写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

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


3

也许可以使用或有助于

https://thorsteinssonh.github.io/bash_test_tools/

打算用TAP协议编写结果,我认为这对CI很有用,对于希望使用shell环境的人也很有用。我想有些事情在外壳环境中运行,因此,有些人可能认为应该在其外壳环境中进行测试。


3

尝试assert.sh

source "./assert.sh"

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

希望能帮助到你!


3

我不敢相信没有人谈论OSHT!它与TAP和JUnit 兼容,它是纯外壳程序(也就是说,不涉及其他任何语言),它也可以独立工作,并且简单直接。

测试如下所示(摘录自项目页面的片段):

#!/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,因为它是a 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。对我而言,此框架以最少的开销提供了最大的收益。


1

我已经尝试了这里介绍的许多解决方案,但是发现其中大多数解决方案体积庞大且难以使用,因此我建立了自己的小型测试框架:https : //github.com/meonlol/t-bash

这只是回购中的一个文件,您可以直接运行,并具有一组基本的JUnit样式断言。

我已经在几个内部项目中专业地使用过它,并且能够使我们的bash脚本具有超强的稳定性和抗回归性。




-2

Python具有如此巨大的优势时,我发现很难为大型脚本使用bash :

  • Try / Except允许编写更强大的脚本,并能够在发生错误时撤消更改。
  • 您不必使用晦涩的语法,例如“if [ x"$foo" = x"$bar"]; then ...容易 '。
  • 使用以下命令轻松解析选项和参数 getopt模块可以(还有一个用于解析参数的更简单的模块,但是名称使我无所适从)。
  • Python使您可以使用列表/字典和对象,而不是基本的字符串和数组。
  • 访问适当的语言工具(例如正则表达式,数据库)(确保可以将所有内容通过mysqlbash传递到命令中,但这不是编写代码的最佳方法)。
  • 不必担心使用$*or或"$*"or "$@"$1or 的正确格式,"$1"文件名中的空格不是问题,等等,等等,等等。

现在,我仅将bash用于最简单的脚本。


3
不能否认Python具有优势,但是您的第二点不是很好。可以做与相同的比较if [[ $foo = $bar ]]; then ...。这仍然不比python所提供的要好,但是比您介绍的要好。
Shrikant Sharat 2011年

8
一些系统(例如嵌入)没有可用的python,您不能/不希望安装其他东西。
Rui Marques 2012年

2
我个人很喜欢bash,但是同意它可能有点危险。通常,您必须更加主动,而在Python中,您可以在出现错误后解决它们。但是,bash确实具有trap(在发生错误时进行清除/撤消)以及正则表达式(即[[ $1 =~ ^[1-3]{3}$ ]])。我很确定您使用的晦涩语法是参考而test不是bash的旧实现。Bash非常适合与现有的命令行工具进行交互...通常,使用单个管道连接Python awkgrep比使用Python替代要容易得多。
2015年

1
顺便说一句,您所引用的解析器模块可能是optparse其继任者argparse。我从未见过有人使用该getopt模块,也从未亲自使用过该模块。该getopt实用程序是伟大的。一旦有了良好的模式,从shell解析参数根本就不是问题。除非您尝试实现git-style子命令之类的东西,否则没有什么麻烦。
2015年

Python不会在bash可以到达的任何地方运行。我说这是因为我们测试了bash与python,相同的代码逻辑,并要求两者都做某事。Bash输入了它有权访问的每个目录。另一方面,python无法处理某些目录权限和文件,以及无法快速增加和减少的目录。
vianna77 '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.