拥有此神技,脚本调试今后与 echo、set、test 说分手

为何要为 Bash 脚本写单元测试?

由于 Bash 脚本一般都是在执行一些与操做系统有关的操做,可能会对运行环境形成一些不可逆的操做,好比修改或者删除文件、升级系统中的软件包等。git

因此为了确保 Bash 脚本的安全可靠,在生产环境中部署以前必定须要作好足够的测试以确保其行为符合咱们的预期。github

如何可以安全可靠的去测试 Bash 脚本呢?有人可能会说咱们能够用 Docker 容器。是的,这样作即安全又方便。在容器隔离出来的环境中不用担忧脚本会破坏咱们的系统,并且也能很是简单的快速重建出一个可用的测试环境。编程

不过呢,请考虑如下的几个常见的场景:数组

  • 场景一:在执行 Bash 脚本测试前,咱们须要须要事先安装好全部在 Bash 脚本中会用到的第三方工具,不然这些测试将会由于命令找不到而执行失败。例如,咱们在脚本中使用了 Bazel 这个构建工具。咱们必须提早安装并配置好 Bazel,并且不要忘记为了可以正常使用 Bazel 还得须要一个支持使用 Bazel 构建的工程。
  • 场景二:测试结果的稳定性可能取决于脚本中访问的第三方服务的稳定性。好比,咱们在脚本中使用 curl 命令从一个网络服务中获取数据,但这个服务有时候可能会访问失败。有多是由于网络不稳定致使的,也多是由于这个服务自己不稳定。再或者若是咱们须要第三方服务返回不一样的数据以便测试脚本的不一样分支逻辑,但咱们可能很难去修改这个第三方服务的数据。
  • 场景三:Bash 脚本的测试用例的执行时间取决于脚本中使用的命令的执行时间。例如,若是咱们中脚本中使用了 Gradle 来构建一个工程,因为不一样的工程大小 Gradle 的一个构建可能要执行3分钟或者3个小时。这还只是一个测试用例,若是咱们还有20个或者100个测试用例呢?咱们是否还能在几秒内得到测试报告呢?

即便使用了容器来执行 Bash 脚本测试,也同样没法避免上面的几个问题。环境的准备过程可能会随着测试用例的增多而变的繁琐,测试用例的稳定性和执行时长取决于第三方命令和服务的稳定性和执行时长,还可能很难作到使用不一样数据来覆盖不一样的测试场景。安全

对于测试 Bash 脚原本说,咱们真正要验证的是 Bash 脚本的执行逻辑。好比在 Bash 脚本中可能会根据传入的参数来组合出内部所调用的命令的选项和参数,咱们要验证的是这些选项和参数确实如咱们预期的。至于调用的命令在接受了这些选项和参数后因为什么缘由而失败,可能咱们并不关心这全部的可能缘由。bash

由于这会有更多的外部影响因素,好比硬件和网络都是否工做正常、第三方服务是否正常运行、构建工程所需的编译器是否安装并配置稳当、受权和认证信息是否都有效、等等。但对于 Bash 脚原本说,这些外部缘由致使的结果就是所调用的命令执行成功或者失败了。因此 Bash 脚本只要关注的是脚本中调用的命令是否可以成功执行,以及命令输出了哪些,并决定随后执行脚本中的哪些不一样分支逻辑。

若是说咱们就是想知道这个命令搭配上这些选项参数是否能按咱们预期的那样工做呢?很简单,那就单独在命令行里面去执行一下。若是在命令行中也不能按预期的工做,放到 Bash 脚本里面也同样不会按预期的工做。这种错误和 Bash 脚本几乎没什么关系了。网络

因此,为了尽可能去除影响 Bash 脚本验证的那些外部因素,咱们应该考虑为 Bash 脚本编写单元测试,以关注在 Bash 脚本的执行逻辑上。框架

什么样的测试才是 Bash 脚本的单元测试?

首先,全部存在于 PATH 环境变量的路径中的命令都不该该在单元测试中被执行。对 Bash 脚原本说,被调用的这些命令能够正常运行,有返回值,有输出。但脚本中调用的这些命令都是被模拟出来的,用于模拟对应的真实命令的行为。这样,咱们在 Bash 脚本的单元测试中就避免了很大一部分的外部依赖,并且测试的执行速度也不会受到真实命令的影响了。curl

其次,每一个单元测试用例之间都应该是独立的。这意味着,这些测试用例能够独立执行或者被任意乱序执行,而不会影响验证结果。编程语言

最后,这些测试用例能够在不一样的操做系统上执行,且都应该获得相同的验证结果。好比 Bash 脚本中使用了只有 GNU/Linux 上才有的命令,对应的单元测试也能够在 Windows 或者 macOS 上执行,且结果一致。

怎样为 Bash 脚本写单元测试?

与其余编程语言同样,Bash 也有多个测试框架,好比 Bats、Shunit2 等,但这些框架实际上并不能隔离全部 PATH 环境变量中的命令。有一个名为 Bach Testing Framework 的测试框架是目前惟一一个能够为 Bash 脚本编写真正的单元测试的框架。

Bach Testing Framework 的最独特的特性就是默认不会执行任何位于 PATH 环境变量中的命令,所以 Bach Testing Framework 很是适用于验证 Bash 脚本的执行逻辑。而且还带来了如下好处:

  • 简单
    什么也不用安装。咱们就能够执行这些测试。好比能够在一个全新的环境中执行一个调用了大量第三方命令的 Bash 脚本。

  • 由于全部的命令都不会被真正执行,因此每个测试用例的执行都很是快。
  • 安全
    由于不会执行任何外部的命令,因此即便由于 Bash 脚本中的某些错误致使执行了一个危险的命令,好比 rm -rf *。Bach 会保证这些危险命令不会被执行。
  • 与运行环境无关
    能够在 Windows 上去执行只能工做在 GNU/Linux 上的脚本的测试。

因为操做系统和 Bash 的一些限制,Bach Testing Framework 没法作到:

  • 拦截使用绝对路径调用的命令
    事实上咱们应该避免在 Bash 脚本中使用绝对路径,若是不可避免的要使用,咱们能够把这个绝对路径抽取为一个变量,或者放入到一个函数中,而后用 @mock API 去模拟这个函数。
  • 拦截诸如 >>><< 等等这样的 I/O 重定向
    是的,没法拦截 I/O 重定向。咱们也一样能够把这些重定向操做隔离到一个函数中,而后再模拟这个函数。

Bach Testing Framework 的使用

Bach Testing Framework 须要 Bash v4.3 或更高版本。在 GNU/Linux 上还须要 Coreutils 和 Diffutils,在经常使用的发行版中都已经默认安装好了。

Bach 在 Linux/macOS/Cygwin/Git Bash/FreeBSD 等操做系统或者运行环境中验证经过。

  • Bash v4.3+
  • Coreutils (GNU/Linux)
  • Diffutils (GNU/Linux)

安装 Bach Testing Framework

Bach Testing Framework 的安装很简单,只须要下载 https://github.com/bach-sh/ba... 到你的项目中,在测试脚本中用 source 命令导入 Bach Testing Framework 的 bach.sh 便可。

好比:

source path/to/bach.sh

一个简单的例子

与其它的测试框架不一样,Bach Testing Framework 的每个测试用例都是由两个 Bash 函数组成,一个是以 test- 开头的测试执行函数,另外一个是同名的以 -assert 结尾的测试验证函数。

好比在下面的例子中,有两个测试用例,分别是

– test-rm-rf
– test-rm-your-dot-git

一个完整的测试用例:

#!/usr/bin/env bash
set -euo pipefail
source bach.sh # 导入 Bach Testing Framework
test-rm-rf() {
 # Bach 的标准测试用例是由两个方法组成
 #   - test-rm-rf
 #   - test-rm-rf-assert
 # 这个方法 `test-rm-rf` 是测试用例的执行
 project_log_path=/tmp/project/logs
 sudo rm -rf "$project_log_ptah/" # 注意,这里有个笔误!}
test-rm-rf-assert() {
 # 这个方法 `test-rm-rf-assert` 是测试用例的验证
 sudo rm -rf /   # 这就是真实的将会执行的命令
 # 不要慌!使用 Bach 测试框架不会让这个命令真的执行!}
test-rm-your-dot-git() {
 # 模拟 `find` 命令来查找你的主目录下的全部 `.git` 目录,假设会找到两个目录
 @mock find ~ -type d -name .git === @stdout ~/src/your-awesome-project/.git 
 ~/src/code/.git
 # 开始执行!删除你的主目录下的全部 `.git` 目录!find ~ -type d -name .git | xargs -- rm -rf
}
test-rm-your-dot-git-assert() {
 # 验证在 `test-rm-your-dot-git` 这个测试执行方法中最终是否会执行如下这个命令。rm -rf ~/src/your-awesome-project/.git ~/src/code/.git
}

Bach 会分别运行每个测试用例的两个方法,去验证两个方法中执行的命令及其参数是不是一致的。

好比,第一个方法 test-rm-rf 是 Bach 的测试用例的执行,与之对应的测试验证方法就是 test-rm-rf-assert 这个方法

在第二个测试用例 test-rm-your-dot-git 中使用了 @mock API 来模拟了命令 find ~ type d -name .git 的行为,这个命令用来找出用户目录下的全部 .git 目录。模拟以后,这个命令并不会真的执行,而是利用了 @stdout API 在标准终端上输出了两个虚拟的目录名。

而后咱们就能够执行真正的命令了,将 find 命令的输出结果传递给 xargs 命令,并组合到 rm -rf 命令以后。

在对应的测试验证函数 test-rm-your-dot-git-assert 里面就验证是 find ~ -type d -name .git | xargs -- rm -rf 的运行结果是否等同于命令 rm -rf ~/src/your-awesome-project/.git ~/src/code/.git

@mock 是 Bach Testing Framework 中很重要的一个 API,利用这个 API 咱们就能够模拟 Bash 脚本中所使用的任意命令的行为或者输出。

好比

@mock curl --silent google.com === 
 @stdout "baidu.com"

模拟了命令 curl --silent google.com 的执行结果是输出 baidu.com。在真实的正常场景下,咱们是没法作到访问 google.com 获得的是 baidu.com。这样模拟以后就能够用来验证 Bash 脚本中处理一个命令不一样响应时的行为了。

@mock API 甚至还支持更复杂的行为模拟,咱们能够自定义一个复杂的模拟逻辑,好比:

@mock ls <<CMD
 if [[ "$var" -eq 1 ]]; then
 @stdout one
 else
 @stdout others
 fi
CMD

在这个模拟中,会根据变量 $var 的值来决定命令 ls 的输出 one 仍是 others

@mock API 模拟的命令在任什么时候候执行的时候都是一样的行为。但若是要模拟同一个命令重复执行的时候要返回不一样的值,Bach Testing Framework 还提供了一个 @@mock 这个 API,好比:

@@mock uuid === @stdout aaaa-1111-2222
@@mock uuid === @stdout bbbb-3333-4444
@@mock uuid === @stdout cccc-5555-6666

这三个模拟命令模拟了 uuid 在重复执行三次的时候都返回不一样的结果,按照模拟的前后顺序分别输出对应的模拟输出。若是在执行完全部的模拟输出后,再重复执行将会始终输出最后一个模拟的输出。

更详细的 API 介绍请在 Bach Testing Framework 的官网 https://bach.sh 查看。

使用 Bach Testing Framework 还可让咱们更安全方便的练习 Bash 编程。

好比,咱们但愿实现一个函数 cleanup 用来删除参数上指定的文件。一个实现多是:

function cleanup() {
 rm $1
}

这个函数的实现实际上是有安全问题的,由于对于 Bash 来讲,有没有把一个变量用双引号包含起来是很是重要的。在这个实现中,变量 $1 就没有用双引号,这会带来严重的后果。下面咱们将使用 @touch API 来建立几个文件,其中将有一个文件名中含有特殊字符 的文件 bar。

咱们都知道,对于含有特殊字符的文件名是要放入到双引号中的。如今这个这个 cleanup 的实现里面没有使用双引号,可是传参的时候使用了双引号,那是否还会按照咱们的预期来执行呢?

function cleanup() {
 rm -rf $1
}
test-learn-bash-no-double-quote-star() {
 # 建立了三个文件,其中有一个名为 "bar*" 的文件
 @touch bar1 bar2 bar3 "bar*"
 # 要删除这个错误的文件名 bar*,而不删除其余文件,使用了双引号来传参,这是正确的
 cleanup "bar*"
}
test-learn-bash-no-double-quote-star-assert() {
 rm -rf "bar*"
}

这个测试用例将会失败,从验证结果中咱们能够看到,指望只删除文件 bar,可是在函数 cleanup 里面,由于遗漏了双引号,会致使变量被二次展开。实际执行的命令是 rm -rf "bar*" bar1 bar2 bar3

如今修复函数 cleanup,把变量 $1 放入双引号:

function cleanup() {
 rm -rf "$1"
}

再次执行测试,会发现确实执行的是命令 rm -rf "bar*"

Bach Testing Framework 目前已经在宝马集团和华为内部使用了。在宝马集团的一个有数千人规模的大型项目里,Bach Testing Framework 保证了数个很是重要的构建脚本的维护。

这些脚本的可靠性和稳定性决定了数千人团队的工做效率,如今就能够在本地快速验证这些构建脚本的执行逻辑,也避免了在本地很难复现一些构建集群中的特殊场景的问题。

做者:柴锋
原文连接: https://chaifeng.com/unit-tes...

image