为什么要为 Bash 脚本写单元测试?
因为 Bash 脚本通常都是在执行一些与操作系统无关的操作,可能会对运行环境造成一些不可逆的操作,比方批改或者删除文件、降级零碎中的软件包等。
所以为了确保 Bash 脚本的安全可靠,在生产环境中部署之前肯定须要做好足够的测试以确保其行为合乎咱们的预期。
如何可能安全可靠的去测试 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 脚本的单元测试?
首先,所有存在于 PATH
环境变量的门路中的命令都不应该在单元测试中被执行。对 Bash 脚本来说,被调用的这些命令能够失常运行,有返回值,有输入。但脚本中调用的这些命令都是被模仿进去的,用于模仿对应的实在命令的行为。这样,咱们在 Bash 脚本的单元测试中就防止了很大一部分的内部依赖,而且测试的执行速度也不会受到实在命令的影响了。
其次,每个单元测试用例之间都应该是独立的。这意味着,这些测试用例能够独立执行或者被任意乱序执行,而不会影响验证后果。
最初,这些测试用例能够在不同的操作系统上执行,且都应该失去雷同的验证后果。比方 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…