【问题标题】:Unit testing Bash scripts单元测试 Bash 脚本
【发布时间】:2010-11-23 06:57:09
【问题描述】:

我们的系统除了 Java 代码之外还运行一些 Bash 脚本。由于我们正在尝试测试所有可能损坏的东西,而那些 Bash 脚本可能会损坏,我们想要测试它们。

问题是很难测试 Bash 脚本。

是否有测试 Bash 脚本的方法或最佳实践?还是应该停止使用 Bash 脚本并寻找可测试的替代解决方案?

【问题讨论】:

标签: bash testing tdd automated-tests extreme-programming


【解决方案1】:

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

之前有人问过类似的问题:

【讨论】:

  • 我可以断言(双关语) shunit2(版本 2.1.6)迄今为止有点损坏。 assertNull 和 assertNotNull 不起作用,即使您直接向它们提供值。 assertEquals 工作正常,但我想我现在只能自己动手了。
  • @labyrinth,您确定问题不是这种情况:github.com/kward/shunit2/issues/53“如何正确使用 assertNull?”?
  • @Victor 绝对有可能我对双引号不够小心。我很快就会回到 shunit2 或一些 bash 单元测试系统将非常有用的角色。我会再试一次。
  • 我是 shunit2 的用户,有时也是贡献者,我可以确认该项目在 2019 年依然活跃。
【解决方案2】:

TAP 兼容 Bash 测试:Bash Automated Testing System

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

【讨论】:

  • 值得披露 TAP 是什么以及为什么要关心,否则它只是毫无意义的复制粘贴
  • @om-nom-nom:我现在将它链接到 TAP 网站。
  • 因为没有人说不可言说:TAP = 测试任何协议
【解决方案3】:

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

可以导入(包括, 无论如何)一个过程(函数, 不管它叫什么名字)从外部 文件。这是写一篇文章的关键 测试脚本:你打破你的 脚本成独立的程序 然后可以导入到两者中 你的运行脚本和你的测试 脚本,然后你就可以运行了 脚本尽可能简单。

这种方法就像dependency injection 的脚本,听起来很合理。最好避免使用 Bash 脚本并使用更可测试且不那么晦涩的语言。

【讨论】:

  • 我不确定我应该投赞成票还是反对票,一方面划分成更小的部分是好的,但另一方面我需要一个框架而不是一组自定义脚本
  • 虽然 bash 没有什么问题(我写了很多很多脚本),但它是一门很难掌握的语言。我的经验法则是,如果脚本大到需要测试,您可能应该转向易于测试的脚本语言。
  • 但有时你需要有一些可以在用户的​​ shell 中获取的东西。我不清楚如果不使用 shell 脚本你会如何做到这一点
  • @Itkovian - 例如,您可以使用 npm 将可执行文件导出到路径,因此不需要采购(您的 npm 包必须全局安装)
  • 我将遵循关于不使用 bash 的建议。 :)
【解决方案4】:

Nikita Sobolev 写了一篇优秀的博文,比较了几个不同的 Bash 测试框架:Testing Bash applications

对于不耐烦的人:Nikita 的结论是使用 Bats,但似乎 Nikita 错过了 Bats-core 项目,在我看来这是一个可以使用的项目,因为最初的 Bats 项目从那时起就没有得到积极维护2013.

【讨论】:

    【解决方案5】:

    我不敢相信没有人谈论OSHT!它兼容两者 TAPJUnit,它是纯 shell(即不涉及其他语言),它也可以独立工作,而且简单直接。

    测试看起来像这样(sn-ps 取自项目页面):

    #!/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 运行脚本。对我来说,这个框架以最少的开销提供了最大的收益。

    【讨论】:

      【解决方案6】:

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

      它由纯 POSIX shell 脚本编写。它已经在 shunit2 以外的许多 shell 上进行了测试。它比 bats/bats-core 拥有更强大的功能。

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

      查看项目页面上的演示。

      【讨论】:

        【解决方案7】:

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

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

        我将a presentationBeakerLib(Red Hat 某些人使用的框架)进行了比较。

        【讨论】:

          【解决方案8】:

          为什么说测试 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
           }
          

          【讨论】:

          • 首先,bash 脚本的可读性不高。其次,期望很复杂,例如检查是否使用创建它的 bash 脚本的 PID 创建了锁定文件。
          • 更重要的是,很难测试 shell 脚本,因为它们通常有大量的副作用,并且会利用文件系统、网络等系统资源。理想情况下,单元测试是无副作用的,并且可以不依赖于系统资源。
          【解决方案9】:

          试试assert.sh

          source "./assert.sh"
          
          local expected actual
          expected="Hello"
          actual="World!"
          assert_eq "$expected" "$actual" "not equivalent!"
          # => x Hello == World :: not equivalent!
          

          【讨论】:

            【解决方案10】:

            我非常喜欢shell2junit,这是一个从 Bash 脚本测试生成类似JUnit 输出的实用程序。这很有用,因为生成的报告可以被持续集成系统读取,例如 JenkinsBamboo 的 JUnit 插件。

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

            【讨论】:

              【解决方案11】:

              试试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
              

              您可以找到some examples herehere

              【讨论】:

                【解决方案12】:

                也许这可以被使用或贡献于:

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

                它的目的是在 TAP 协议中编写结果,我认为这对CI 有好处,对那些想要 shell 环境的人也有好处。我想有些东西在 shell 环境中运行,所以有些人可能会认为应该在他们的 shell 环境中进行测试。

                【讨论】:

                  【解决方案13】:

                  我尝试了很多这里介绍的解决方案,但我发现它们中的大多数都过于庞大且难以使用,因此我构建了自己的小测试框架:https://github.com/meonlol/t-bash

                  它只是存储库中的一个文件,您可以直接运行它,并带有一组基本的JUnit 样式断言。

                  我在几个内部项目中专业使用过它,并且能够使我们的 Bash 脚本超级稳定和抗回归。

                  【讨论】:

                    【解决方案14】:

                    你可能想看看 bash_unit:

                    https://github.com/pgrange/bash_unit

                    【讨论】:

                      【解决方案15】:

                      看看Outthentic。它很简单,可通过多种语言(PerlPythonRuby 和 Bash on choice)和跨平台(Linux 和 Windows)框架来测试任何命令行应用程序。

                      【讨论】:

                        【解决方案16】:

                        创建一个测试mytest.sh。它使用特定的输入调用您的脚本。

                        创建一个文本文件expected/mytest.stdout。它包含测试的预期输出。

                        将它们提交给版本控制(如果有的话)。

                        运行测试并将输出重定向到另一个文件:

                        mytest.sh > actual/mytest.stdout
                        

                        然后我们只需要一个文本差异工具来查看结果偏离的地方。我认为你可以这样做

                        diff expected/mytest.stdout actual/mytest.stdout
                        

                        【讨论】:

                          猜你喜欢
                          • 2012-02-26
                          • 2019-08-06
                          • 2010-11-01
                          • 1970-01-01
                          • 1970-01-01
                          • 1970-01-01
                          • 2011-07-19
                          • 1970-01-01
                          • 2015-07-02
                          相关资源
                          最近更新 更多