Shell脚本的单元测试


78

多年来,我从事的几乎所有产品都涉及一定程度的Shell脚本(或Windows上的批处理文件,PowerShell等)。即使我们用Java或C ++编写了大量代码,但似乎总是存在一些集成或安装任务,而这些任务最好使用Shell脚本来完成。

这样,shell脚本便成为随附代码的一部分,因此需要像已编译的代码一样进行测试。有没有人对其中的一些shell脚本单元测试框架有经验,例如shunit2?目前,我主要对Linux shell脚本感兴趣。我想知道测试工具能够很好地复制其他xUnit框架的功能和易用性,以及与持续构建系统(如CruiseControl或Hudson)集成的难易程度。


综述为您确定了一些任务/标签。一旦克服了学习障碍,它就会非常有用。我个人更喜欢haridsv的方法,因为它不需要我安装另一个pkg。我已经将这种方法应用于shell脚本和python测试。
待办事项

1
您还可以检查bashtest实用程序:github.com/pahaz/bashtest(这是编写bash测试的简单方法)
pahaz

1
结帐几乎每一个可能的工具的概述:medium.com/wemake-services/...
sobolevn

@sobolevn非常好的文章,谢谢!
gareth_bowles

Answers:


54

更新2019-03-01:我的偏好是现在蝙蝠。我在小型项目上使用了几年。我喜欢简洁明了的语法。我没有将它与CI / CD框架集成在一起,但是它的退出状态确实反映了该套件的整体成功/失败,这比shunit2更好,如下所述。


上一个答案:

我正在将shunit2用于与Linux环境中的Java / Ruby Web应用程序相关的shell脚本。它易于使用,并且与其他xUnit框架没有很大的不同。

我没有尝试与CruiseControl或Hudson / Jenkins集成,但是在通过其他方式实施持续集成时遇到了以下问题:

  • 退出状态:当测试套件失败时,shunit2不会使用非零退出状态来传达失败。因此,您要么必须解析shunit2输出以确定套件的通过/失败,要么更改shunit2以使其表现出某些连续集成框架所期望的行为,并通过退出状态传达通过/失败的信息。
  • XML日志:shunit2不会产生JUnit样式的XML结果日志。

皮特,我即将发布一个单元测试库,该库适用于bash和ksh,使用非常简单。您可以在某个地方向我发送指向Junit样式的XML日志的链接,以便我可以看到其中包含的内容吗?如果大于0的测试失败退出1,您是否还希望退出单个失败的测试或退出测试的退出状态?现在,我的方法是可以调用一个函数来获取通过/失败的测试数量。
伊桑(Ethan)发布了

@EthanPost我不再使用/要求XML日志。通过搜索诸如“ xunit xml format”之类的内容,您可能会找到帮助。这样的事情:xunit.github.io/docs/format-xml-v2.html
Pete TerMaat

2
shunit现在也以正确的退出代码退出:github.com/kward/shunit2/blob/…–
Flamefire

32

想知道为什么没人提到BATS。它是最新的,并且与TAP兼容。

描述:

#!/usr/bin/env bats

@test "addition using bc" {
  result="$(echo 2+2 | bc)"
  [ "$result" -eq 4 ]
}

跑:

$ bats addition.bats
 ✓ addition using bc

1 tests, 0 failures

1
BATS +1!从其github指标(12位贡献者,近2000颗星)来看,这是大多数人选择作为bash xUnit测试运行程序的工具。
Noah Sussman 2015年

1
+1:我前一段时间使用过BATS,但并没有完全不知所措。今天找到了这份出色的(独立的?)《入门指南》文档,一直沿用至今,现已在BATS上完全出售。
ssc

当我读完这个答案时,我的第一个冲动是“不,维护人员没有响应问题,并且存在一些严重的错误,但是后来我发现,自从我上次使用它以来,它已经被分叉了”
Andy

22

@ blake-mizerany的综述听起来很棒,将来我应该使用它,但这是我用于创建单元测试的“穷人”方法:

  • 将所有可测试的功能分开。
  • 移动功能整合到一个外部文件,说functions.shsource成剧本。您可以source `dirname $0`/functions.sh用于此目的。
  • 在的末尾functions.sh,如果满足条件,则将测试用例嵌入以下内容:

    if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    fi
    
  • 您的测试是对函数的文字调用,然后简单检查出口代码和变量值。我喜欢添加如下所示的简单实用程序函数,以使其易于编写:

    function assertEquals()
    {
        msg=$1; shift
        expected=$1; shift
        actual=$1; shift
        if [ "$expected" != "$actual" ]; then
            echo "$msg EXPECTED=$expected ACTUAL=$actual"
            exit 2
        fi
    }
    
  • 最后,functions.sh直接运行以执行测试。

这是显示此方法的示例:

    #!/bin/bash
    function adder()
    {
        return $(($1+$2))
    }

    (
        [[ "${BASH_SOURCE[0]}" == "${0}" ]] || exit 0
        function assertEquals()
        {
            msg=$1; shift
            expected=$1; shift
            actual=$1; shift
            /bin/echo -n "$msg: "
            if [ "$expected" != "$actual" ]; then
                echo "FAILED: EXPECTED=$expected ACTUAL=$actual"
            else
                echo PASSED
            fi
        }

        adder 2 3
        assertEquals "adding two numbers" 5 $?
    )

1
很好,谢谢-我真的很喜欢shell脚本的结构化编程方法。
gareth_bowles

那里的观察非常棒:“将所有可测试的功能分开。” 即使您还没有为它编写测试,这一点也很重要。这是提高代码可读性的主要因素。几年前,我开始根据此原则编写(ksh)脚本。将所有内容编写为函数。
Henk Langeveld

@HenkLangeveld也不会exit 0导致“ sourcee”(采购此商品的人)退出吗?我想你在return这里找。
haridsv

@haridsv好问题。在获取文件源时,您不想运行测试。通过将所有与测试相关的代码收集到一个子外壳中,我们不会破坏“消费者”的运行时环境。那只会exit 0终止子shell。
Henk Langeveld

1
太好了 感谢您的详细解释。
iconoclast

10

除了综合报道shunit2我的壳单元测试工具概述还包括assert.shshelltestrunner

我大体上同意综述作者对shunit2的批评(其中有些是主观的),因此在查看了文档和示例后,我排除了shunit2。虽然,对jUnit有一定的经验确实很熟悉。

我认为shelltestrunner是我看过的最原始的工具,因为它使用简单的声明式语法来定义测试用例。像往常一样,任何抽象级别都以灵活性为代价提供了一些便利。即使如此,简单性还是很吸引人,我发现该工具对我的情况而言过于局限,主要是因为缺乏定义setup / tearDown操作的方法(例如,在测试之前操作输入文件,在测试之后删除状态文件)等)。

起初,我有点困惑,assert.sh只允许声明输出或退出状态,而我同时需要两者。时间足够长,可以使用总结来编写几个测试用例。但是我很快发现综述的set -e模式很不方便,因为在某些情况下,除了stdout之外,还希望将非零退出状态作为一种传递结果的方法,这会使测试用例在所述模式下失败。示例之一显示了解决方案:

status=$(set +e ; rup roundup-5 >/dev/null ; echo $?)

但是,如果我既需要非零退出状态又需要输出怎么办?我当然可以set +e在调用之前,整个测试案例set -e之后或set +e整个测试案例中使用。但这违反了综述的原则“一切都是断言”。因此,感觉就像我开始使用该工具一样。

到那时,我已经意识到assert.sh的“缺点”,即只允许声明退出状态或输出实际上是一个非问题,因为我可以通过test这样的复合表达式进行传递

output=$($tested_script_with_args)
status=$?
expected_output="the expectation"
assert_raises "test \"$output\" = \"$expected_output\" -a $status -eq 2"

由于我的需求确实很基本(运行一套测试,显示一切正常或失败),所以我喜欢assert.sh的简单性,因此我选择了它。


7

我最近发布了称为shellspec的新测试框架。

shellspec是BDD样式测试框架。它适用于POSIX兼容的shell脚本,包括bash,破折号,ksh,busybox等。

当然,退出状态反映了规范运行的结果,并且具有TAP兼容格式化程序。

specfile接近自然语言,易于阅读,并且是与Shell脚本兼容的语法。

#shellcheck shell=sh

Describe 'sample'
  Describe 'calc()'
    calc() { echo "$(($*))"; }

    It 'calculates the formula'
      When call calc 1 + 2
      The output should equal 3
    End
  End
End

版本0.16.0中支持的JUnit格式化程序。并支持覆盖率,并行执行等。
中岛浩一(Koichi Nakashima)

3

您应该尝试使用assert.sh lib,非常方便,易于使用

local expected actual
expected="Hello"
actual="World!"
assert_eq "$expected" "$actual" "not equivalent!"
# => x Hello == World :: not equivalent! 
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.