Bats-core bash版自动化测试工具
Bats 是一个符合 TAP 标准 的 Bash 版测试框架,它使用了一种极为简便的方法来验证命令行程序是否正常运行。
Bats 要求 Bash 的最低版本是 3.2.57 ,Bats 测试文件实际上一个 bash 的脚本文件,完全可以使用 shell 的语法书写。
安装
强烈建议使用源码或者npm安装最新版本, Bats 经过多人接手,代码存放比较混乱,某些系统下安装的是旧版本的,目前社区在维护的版本地址为:
1 | https://github.com/bats-core/bats-core.git |
macOS 下安装
1 | brew install bats-core |
CentOS 下安装
1 | yum install bats |
Ubuntu 下安装
1 | apt-get install bats |
Windows 安装
Windows 平台需要 bash 环境可以使用以下任意一个
- Git for Windows Bash (MSYS2 based)
- Windows Subsystem for Linux
- MSYS2
- Cygwin
Windows下推荐使用源码安装或者 npm 的方式进行安装。
使用源码安装
1 | git clone https://github.com/bats-core/bats-core.git |
2 | cd bats-core |
3 | ./install.sh /usr/local |
使用 Docker 安装
1 | docker pull bats/bats |
2 | docker run -it --rm bats/bats --version |
3 | docker run -it --rm -v "$(pwd):/code" bats/bats /code/test |
使用 npm 安装
1 | npm install -g bats |
经过测试 CentOS 下的版本为 2014 年的版本,ubuntu下也不是最新版本,建议使用源码或者npm安装最新版本。
语法
测试用例的语法格式为:
1 | !/usr/bin/env bats |
2 | |
3 | @test "grep --version check" { # 测试用例名称 |
4 | run grep --version # 运行的外部命令 |
5 | [ $status -eq 0 ] # 断言 |
6 | [ "${lines[0]%% *}" == 'grep' ] # 断言 |
7 | } |
每个 Bats 测试文件的评估次数为 n + 1 次,其中 n 是文件中的测试用例数。运行测试脚本时首先计算测试用例的数量,然后遍历测试用例并在独立进程中执行每个测试用例。
在运行测试用例时,Bats使用Bash的 errexit
(set -e
)选项,这样写在@test
里面的语句都是真理断言。一旦测试用例中的某一个断言失败(某条语句的状态码不是 0
)则这个测试用例视为失败。
常用指令
run
:运行外部命令
用于测试外部命令然后对它的退出状态和输出状态进行断言。Bats 包含一个 run
的指令,它可以将传入的参数当成命令调用,并且将退出状态和输出状态保存到特殊的全局变量中,以便可以继续在测试用例中增加断言。
比如说我们正在测试 cat
:
1 | !/usr/bin/env bats |
2 | |
3 | @test "cat nonexistent_filename check" { # 测试用例名称 |
4 | run cat nonexistent_filename # 运行的外部命令 |
5 | [ $status -eq 1 ] # 断言 |
6 | [ "$output" == 'cat: nonexistent_filename: No such file or directory' ] # 断言 |
7 | [ "${lines[0]}" == 'cat: nonexistent_filename: No such file or directory' ] # 断言 |
8 | } |
这里有三个 Bats 的特殊变量
$status
是命令退出状态码
$output
是命令的标准输出和标准错误的内容
$lines
是命令输出内容的数组包含各行内容 cat nonexistent_filename
只有一行内容
load
:使用共享文件
如果想用跨越多个测试文件共享环境变量或者自定义的函数,可以使用 load
指令。共享文件的扩展文件名必须是.bash
。load
可以使用相对路径或者绝对路径。
使用相对路径的写法是(可以省略扩展文件名):
1 | load test_helper |
使用绝对路径的写法时(必须带上扩展文件名):
1 | load /test_helpers/test_helper.bash |
skip
:跳过测试
在测试过程中如果失败时如果想继续可以 skip
指令来跳过测试:
1 | @test "A test I don't want to execute for now" { |
2 | skip |
3 | run foo |
4 | [ "$status" -eq 0 ] |
5 | } |
也可以加入跳过原因:
1 | @test "A test I don't want to execute for now" { |
2 | skip "This command will return zero soon, but not now" |
3 | run foo |
4 | [ "$status" -eq 0 ] |
5 | } |
或者也可以根据条件判断是否跳过:
1 | @test "A test which should run" { |
2 | if [ foo != bar ]; then |
3 | skip "foo isn't bar" |
4 | fi |
5 | |
6 | run foo |
7 | [ "$status" -eq 0 ] |
8 | } |
常用函数
@test
这是 Bats 主要的函数所有的测试用例都要按照这个函数的格式书写:
1 | @test "grep --version check" { # 测试用例名称 |
2 | run grep --version # 运行的外部命令 |
3 | [ $status -eq 0 ] # 断言 |
4 | [ "${lines[0]%% *}" == 'grep' ] # 断言 |
5 | } |
setup
/ teardown
:初始化和善后函数
setup
/ teardown
是两个特殊的函数,用于在测试用例开始之前和结束之后进行初始化和善后工作。比如开始之前设置环境变量创建测试目录。以 soar 为测试用例为例:
1 | setup() { |
2 | export SOAR_DEV_DIRNAME="${BATS_TEST_DIRNAME}/../" |
3 | export SOAR_BIN="${SOAR_DEV_DIRNAME}/bin/soar" |
4 | export SOAR_BIN_ENV="${SOAR_DEV_DIRNAME}/bin/soar -config ${SOAR_DEV_DIRNAME}/etc/soar.yaml" |
5 | export BATS_TMP_DIRNAME="${BATS_TEST_DIRNAME}/tmp" |
6 | export BATS_FIXTURE_DIRNAME="${BATS_TEST_DIRNAME}/fixture" |
7 | mkdir -p "${BATS_TMP_DIRNAME}" |
8 | } |
9 | |
10 | teardown(){ |
11 | //TODO |
12 | ...... |
13 | } |
特殊变量
Bats 中包含几个全局变量 :
$BATS_TEST_FILENAME
Bats测试文件的绝对路径。$BATS_TEST_DIRNAME
Bats测试文件所在的目录。$BATS_TEST_NAMES
每个测试用例的函数名称数组。$BATS_TEST_NAME
包含当前测试用例的函数的名称。$BATS_TEST_DESCRIPTION
当前测试用例的描述。$BATS_TEST_NUMBER
测试文件中当前测试用例的(从1开始)索引。$BATS_TMPDIR
用于存储临时文件的目录的位置。
注意事项
写在 @test
之外的代码
写在 @test
函数之外代码一旦失败 Bats 会立刻中断执行,某些情况下这样做会很有用比如检查依赖项,但是如果在@test
、setup
、teardown
之外打印的任何输出必须重定向到stderr(>&2),否则这些输出内容可能会污染TAP
流导致Bats 测试失败。
经过测试 @test
之外的代码会优先执行
文件描述符 3 (FD3)(如果 Bats 卡死可以读这一块的内容)
Bats 将测试代码的输出流和 TAP 输出流分开,这样做的目的是为了确保 TAP 的输出不被污染。在输出至终端的部分详细介绍了如何使用 FD3 正确打印自定义文本。
但是使用 FD3 的一个已知的问题是:在某些情况下(如程序的子进程在后台运行的时候),它会导致 Bats 卡死。在这种情况下在生成子进程的时候,子进程会从父进程继承 FD3 ,导致Bats 会等待子进程执行完成之后关闭 FD3 。如果子进程需要花费大量时间完成,例如,如果子进程是 sleep 100
命令或是后台服务,那么 Bats 也会阻塞同样的时间。
为了避免这种情况,启动可能长时间运行的子进程之前显式关闭 FD3 command_name 3>&-
。
举例说明:
会卡死的情况:
1 | @test "cat nonexistent_filename check" { # 测试用例名称 |
2 | run cat nonexistent_filename |
3 | sleep 100 & # 后台执行 |
4 | [ $status -eq 1 ] |
5 | [ "$TTTT" -eq 1 ] # 断言 |
6 | [ "$output" == 'cat: nonexistent_filename: No such file or directory' ] # 断言 |
7 | } |
不会卡死的情况:
1 | @test "cat nonexistent_filename check" { # 测试用例名称 |
2 | run cat nonexistent_filename |
3 | sleep 100 3>&- & # 后台执行并且关闭文件描述符3 |
4 | [ $status -eq 1 ] |
5 | [ "$TTTT" -eq 1 ] # 断言 |
6 | [ "$output" == 'cat: nonexistent_filename: No such file or directory' ] # 断言 |
7 | } |
命令输出到终端
在
@test
函数内部输出- 如果从
@test
内部输出字符你需要将输出重定向到 FD3 例如echo 'test'>&3
。这时输出将变成 TAP 流的一部分。为了生成100%符合 TAP 流格式的输出,我们推荐的写法是echo '# text' >&3
。否则,在使用分析 TAP 流的第三方工具时可能会遇到意外错误。 - Bats 默认使用友好的输出格式(
-p, --pretty
)。 TAP 流默认不会从 FD3 中输出任何字符 - 无论指定何种输出格式,直接输出到
stdout
、stderr
的文本(例如echo "aaaa"
)会被@test
视为测试的一部分,仅仅在测试失败的时候显示。
- 如果从
在
setup
/teardown
函数内部输出- 这一部分内容输出行为和
@test
中一样。
- 这一部分内容输出行为和
在
@test
或者setup
/teardown
外部输出- 无论输出的字符被重定向到何处(FD1、FD2、FD3)字符都会立刻显示到终端
- 按照这种方式打印的文本将会禁用友好的输出格式(
-p, --pretty
)。此外他也会使得输出不符合 TAP 流规范。 - 由内部管道或者重定向输出到标准错误的字符总会第一时间显示出来。
Bats 命令行用法
bats 命令行用法:
1 | Bats 1.1.0 |
2 | Usage: bats [-cr] [-f <regex>] [-p | -t] <test>... |
3 | bats [-h | -v] |
4 | |
5 | <test> 为一个 bats 测试用例的文件,或者是一个包含后缀名为 .bats 文件的目录 |
6 | |
7 | -c, --count 计算没有运行的测试用例的个数 |
8 | -f, --filter 通过正则表达式指定运行某些测试用例 |
9 | -h, --help 显示帮助信息 |
10 | -p, --pretty 以比较友好的方式展现测试用例的输出结果(默认是使用这种方式) |
11 | -r, --recursive 在子目录中包含测试 |
12 | -t, --tap 以 TAP 格式显示输出结果 |
13 | -v, --version 显示版本号信息 |
14 | |
15 | 更信息见: https://github.com/bats-core/bats-core |
开发工具推荐
在常用的开发工具中安装 Bats 插件,增加代码高亮显示和代码完成提示,提高开发效率。
VS Code
Sublime Text
Vim
其他开发工具
更多开发工具和插件参考: https://github.com/bats-core/bats-core/wiki/Syntax-Highlighting
参考文献
bats-core: https://github.com/bats-core/bats-core/
Bats Evaluation Process:https://github.com/sstephenson/bats/wiki/Bats-Evaluation-Process