简介: Sharness 是一个用 Shell 脚本来编写测试用例的测试框架。本文将具体介绍 Sharness 的构造及测试用例的编写格局,以及语法标准和技巧,教大家如何应用 Sharness 编写测试用例,同时还分享了 Sharness 的扩大性能和我的项目实战。
参加过 Git 我的项目的测试用例开发,为其测试框架的简洁、高效而折服。已经尝试将 Git 测试用例用于其余我的项目:《复用 git.git 测试框架》[1]。不过从 Git 我的项目中剥离测试用例框架还是挺麻烦的。
一次偶尔的机会发现曾经有人(Christian Couder:Gitlab 工程师,Git 我的项目的领导委员会成员之一)曾经将 Git 的测试用例框架剥离进去,成为独立的开源我的项目 Sharness。
有了 Sharness,写测试用例不再是苦差事。
一 Sharness 是什么?
- Sharness 是一个用 Shell 脚本来编写测试用例的测试框架。
- 能够在 Linux、macOS 平台运行测试用例。
- 测试输入合乎 TAP(test anything protocol),因而能够用 sharness 本身工具或 prove 等 TAP 兼容测试夹具(harness)运行。
- 是由 Junio 在 2005 年为 Git 我的项目开发的测试框架,由 Christian Couder (chriscool) 从 Git 中剥离为独立测试框架。
- 地址:https://github.com/chriscool/sharness
二 Sharness 测试框架的长处
简洁
如果要在测试用例中创立 / 初始化一个文件(内容为“Hello, world.”),看看 sharness 实现起来有如许简略:
cat >expect <<-EOF
Hello, world.
EOF
如果要对某利用(hello-world)的输入和预期的 expect 文件进行比拟,雷同则测试用例通过,不同则展现差别。测试用例编写如下:
test_expect_success“app output test”‘cat >expect <<-EOF &&
Hello, world.
EOF
hello-world >actual &&
test_cmp expect actual‘
调试不便
每个测试用例脚本能够独自执行。应用 -v 参数,能够显示具体输入。应用 -d 参数,运行完结后保留用例的长期目录。
能够在要调试的 test case 前面减少 test_pause 语句,例如:
test_expect_success“name”‘<Script…>‘test_pause
test_done
而后应用 -v 参数运行该脚本,会在 test_pause 语句处中断,进入一个蕴含 sharness 环境变量的子 Shell 中,目录会切换到测试用例独自的工作区。调试结束退出 Shell 即返回。
三 Git 我的项目的测试框架结构
Sharness 源自于 Git 我的项目的测试用例框架。咱们先来看看 Git 我的项目测试框架的构造。
Git 我的项目测试相干文件
- 待测利用放在我的项目的根目录。例如 Git 我的项目的待测利用: git 和 git-receive-pack 等。
- 测试框架批改 PATH 环境变量,使得测试用例在调用待测利用(如 git 命令)的时候,优先应用我的项目根目录下的待测利用。
- 测试脚本命名为 tNNNN-.sh,即以字母 t 和四位数字结尾的脚本文件。
- 每一个测试用例在执行时会创立一个独立的长期目录,例如 trash directory.t5323-pack-redundant。测试用例执行胜利,则该目录会被删除。
相干代码参见 [2]。
四 Git 测试脚本的格局
以如下测试脚本为例 [3]:
(1)在文件头,定义 test_description 变量,提供测试用例的简略阐明,通常应用一行文本。本测试用例较为简单,应用了多行文本进行形容。
#!/bin/sh
#
# Copyright (c) 2018 Jiang Xin
#
test_description='Test git pack-redundant
In order to test git-pack-redundant, we will create a number of objects and
packs in the repository `master.git`. The relationship between packs (P1-P8)
and objects (T, A-R) is showed in the following chart. Objects of a pack will
be marked with letter x, while objects of redundant packs will be marked with
exclamation point, and redundant pack itself will be marked with asterisk.
| T A B C D E F G H I J K L M N O P Q R
----+--------------------------------------
P1 | x x x x x x x x
P2* | ! ! ! ! ! ! !
P3 | x x x x x x
P4* | ! ! ! ! !
P5 | x x x x
P6* | ! ! !
P7 | x x
P8* | !
----+--------------------------------------
ALL | x x x x x x x x x x x x x x x x x x x
Another repository `shared.git` has unique objects (X-Z), while other objects
(marked with letter s) are shared through alt-odb (of `master.git`). The
relationship between packs and objects is as follows:
| T A B C D E F G H I J K L M N O P Q R X Y Z
----+----------------------------------------------
Px1 | s s s x x x
Px2 | s s s x x x
'
(2)蕴含测试框架代码。
. ./test-lib.sh
(3)定义全局变量,以及定义要在测试用例中用到的函数封装。
master_repo=master.git
shared_repo=shared.git
# Create commits in <repo> and assign each commit's oid to shell variables
# given in the arguments (A, B, and C). E.g.:
#
# create_commits_in <repo> A B C
#
# NOTE: Avoid calling this function from a subshell since variable
# assignments will disappear when subshell exits.
create_commits_in () {
repo="$1" &&
if ! parent=$(git -C "$repo" rev-parse HEAD^{} 2>/dev/null)
then
... ...
(4)用 test_expect_success 等办法撰写测试用例。
test_expect_success 'setup master repo' 'git init --bare"$master_repo" &&
create_commits_in "$master_repo" A B C D E F G H I J K L M N O P Q R
'
#############################################################################
# Chart of packs and objects for this test case
#
# | T A B C D E F G H I J K L M N O P Q R
# ----+--------------------------------------
# P1 | x x x x x x x x
# P2 | x x x x x x x
# P3 | x x x x x x
# ----+--------------------------------------
# ALL | x x x x x x x x x x x x x x x
#
#############################################################################
test_expect_success 'master: no redundant for pack 1, 2, 3' 'create_pack_in"$master_repo" P1 <<-EOF &&
$T
$A
$B
$C
$D
$E
$F
$R
EOF
create_pack_in "$master_repo" P2 <<-EOF &&
$B
$C
$D
$E
$G
$H
$I
EOF
create_pack_in "$master_repo" P3 <<-EOF &&
$F
$I
$J
$K
$L
$M
EOF
(
cd "$master_repo" &&
git pack-redundant --all >out &&
test_must_be_empty out
)
'
(5)在脚本的结尾,用 test_done 办法完结测试用例。
test_done
五 Sharness 测试框架结构
Sharness 我的项目由 Git 我的项目的测试框架形象而来,我的项目地址:
https://github.com/chriscool/sharness
- 待测利用放在我的项目的根目录。
- 测试脚本命名为 .t,即扩大名为 .t 的脚本文件。
- 每一个测试用例在执行时会创立一个独立的长期目录,例如 trash directory.simple.t。测试用例执行胜利,则该目录会被删除。
- 在 sharness.d 目录下增加自定义脚本,能够扩大 Sharness 框架。即在框架代码加载时,主动加载该目录下文件。
咱们对 Sharness 测试框架做了一些小改变:
- 定制版本对测试框架代码做了进一步封装,框架代码放在独自的子目录。
- 测试脚本的名称复原为和 Git 我的项目测试脚本相似的名称(tNNNN-.sh),即以字母 t 和四位数字结尾的脚本文件。
六 Sharness 测试用例格局
以如下测试脚本为例 [4]:
(1)在文件头,定义 test_description 变量,提供测试用例的简略阐明,通常应用一行文本。
#!/bin/sh
test_description="git-repo init"
(2)蕴含测试框架代码。
. ./lib/sharness.sh
(3)定义全局变量,以及定义要在测试用例中用到的函数封装。
# Create manifest repositories
manifest_url="file://${REPO_TEST_REPOSITORIES}/hello/manifests"
(4)用 test_expect_success 等办法撰写测试用例。
test_expect_success "setup" '
# create .repo file as a barrier, not find .repo deeper
touch .repo &&
mkdir work
'test_expect_success"git-repo init -u" '
(
cd work &&
git-repo init -u $manifest_url
)
'test_expect_success"manifest points to default.xml" '
(
cd work &&
test -f .repo/manifest.xml &&
echo manifests/default.xml >expect &&
readlink .repo/manifest.xml >actual &&
test_cmp expect actual
)
'
(5)在脚本的结尾,用 test_done 办法完结测试用例。
test_done
七 对于 test_expect_success 办法的参数
test_expect_success 能够有两个参数或者三个参数。
当 test_expect_success 办法前面是两个参数时,第一个参数用于形容测试用例,第二个参数是测试用例要执行的命令。
test_expect_success 'initial checksum' '
(
cd bare.git &&
git checksum --init &&
test -f info/checksum &&
test -f info/checksum.log
) &&
cat >expect <<-EOF &&
INFO[<time>]: initialize checksum
EOF
cat bare.git/info/checksum.log |
sed -e "s/\[.*\]/[<time>]/" >actual &&
test_cmp expect actual
'
当 test_expect_success 办法前面是三个参数时,第一个参数是前置条件,第二个参数用于形容测试用例,第三个参数是测试用例要执行的命令。
例如如下有三个参数的测试,第一个参数定义了前置条件,在 CYGWIN 等环境,不执行测试用例。
test_expect_success !MINGW,!CYGWIN \’correct handling of backslashes' '
rm -rf whitespace &&
mkdir whitespace &&
>"whitespace/trailing 1" &&
>"whitespace/trailing 2 \\\\" &&
>"whitespace/trailing 3 \\\\" &&
>"whitespace/trailing 4 \\" &&
>"whitespace/trailing 5 \\ \\" &&
>"whitespace/trailing 6 \\a\\" &&
>whitespace/untracked &&
sed -e "s/Z$//" >ignore <<-\EOF &&
whitespace/trailing 1 \ Z
whitespace/trailing 2 \\\\Z
whitespace/trailing 3 \\\\ Z
whitespace/trailing 4 \\\ Z
whitespace/trailing 5 \\ \\\ Z
whitespace/trailing 6 \\a\\Z
EOF
echo whitespace/untracked >expect &&
git ls-files -o -X ignore whitespace >actual 2>err &&
test_cmp expect actual &&
test_must_be_empty err
'
八 Sharness 语法标准和技巧
应用 && 级联各个命令,确保所有命令都全副执行胜利
test_expect_success 'shared: create new objects and packs' 'create_commits_in"$shared_repo" X Y Z &&
create_pack_in "$shared_repo" Px1 <<-EOF &&
$X
$Y
$Z
$A
$B
$C
EOF
create_pack_in "$shared_repo" Px2 <<-EOF
$X
$Y
$Z
$D
$E
$F
EOF
'
自定义办法,也要应用 && 级联,确保命令全副执行胜利
create_pack_in () {
repo="$1" &&
name="$2" &&
pack=$(git -C "$repo/objects/pack" pack-objects -q pack) &&
eval $name=$pack &&
eval P$pack=$name:$pack
}
波及到目录切换,在子 Shell 中进行,免得影响后续测试用例执行时的工作目录
test_expect_success 'master: one of pack-2/pack-3 is redundant' 'create_pack_in"$master_repo" P4 <<-EOF &&
$J
$K
$L
$M
$P
EOF
create_pack_in "$master_repo" P5 <<-EOF &&
$G
$H
$N
$O
EOF
(
cd "$master_repo" &&
cat >expect <<-EOF &&
P3:$P3
EOF
git pack-redundant --all >out &&
format_packfiles <out >actual &&
test_cmp expect actual
)
'
函数命名要有意义
如下内容是 Junio 在代码评审时,对测试用例中定义的 format_git_output 办法的评审意见。Junio 指出要在给函数命名时,要应用更有意义的名称。
> +format_git_output () {
Unless this helper is able to take any git output and massage,
please describe what kind of git output it is meant to handle.
Also, "format" does not tell anything to the readers why it wants to
transform its input or what its output is supposed to look like. It
does not help readers and future developers.
Heredoc 的小技巧
应用 heredoc 创立文本文件,如果其中的脚本要定义和应用变量,要对变量中的 $ 字符进行转移。Junio 给出了一个 heredoc 语法的小技巧,能够无需对 $ 字符本义。
> +
> + # setup pre-receive hook
> + cat >upstream/hooks/pre-receive <<-EOF &&
Wouldn't it make it easier to read the resulting text if you quoted
the end-of-here-text marker here, i.e. "<<\-EOF"? That way, you can
lose backslash before $old, $new and $ref.
> + #!/bin/sh
> +
> + printf >&2 "# pre-receive hook\n"
> +
> + while read old new ref
> + do
> + printf >&2 "pre-receive< \$old \$new \$ref\n"
> + done
> + EOF
Shell 编程语法标准
Git 我的项目对于 Shell 编写的测试用例制订了语法标准,例如:
- 应用 tab 缩进。
- 规定 case 语句、if 语句的缩进格局。
- 输入输出重定向字符前面不要有空格。
- 应用 $(command) 而不是
command
。 - 应用 test 办法,不要应用 […]。
残缺语法标准参考 [5]。
九 sharness 常见的内置函数
- test_expect_success
开始一个测试用例。
- test_expect_failure
标记为存在已知问题,执行失败不报错,执行胜利则正告该 broken 的用例曾经 fixed。
- test_must_fail
前面的一条命令必须失败。如果前面命令胜利,测试失败。
- test_expect_code
命令以给定返回值完结。
- test_cmp
比拟两个文件内容,雷同胜利,不同失败并显示差别。
- test_path_is_file
参数必须是一个文件,且存在。
- test_path_is_dir
参数必须是一个目录,且存在。
- test_must_be_empty
参数指向的文件内容必须为空。
- test_seq
跨平台的 seq,用户生成数字序列。
- test_pause
测试暂停,进入子 Shell。
- test_done
测试用例完结。
十 扩大 Sharness
Sharness 提供了扩大性能。用户在 sharness.d 目录中增加以 .sh 结尾的脚本文件,即可对 Sharness 进行扩大。例如:
- 在 trash directory.* 目录下执行 git init 命令。目标是防止目录逃逸时误执行 git 命令影响我的项目自身代码。
例如:测试脚本在工作区下创立了一个仓库(git init my.repo),想要在该仓库下执行 git clean,却忘了进入到 my.repo 子目录再执行,后果导致待测试项目中失落文件。
- 引入 Git 我的项目中的一些有用的测试方法。
如:test_tick 办法,能够设置 GIT_AUTHOR_DATE、GIT_COMMITTER_DATE 等环境变量,确保测试脚本屡次运行时提交工夫的一致性,进而产生统一的提交 ID。
- 引入我的项目须要的其余自定义办法。
例如 git-repo 我的项目为了防止工作区逃逸,在 trash directory.* 目录下创立 .repo 文件。
十一 Sharness 在我的项目中的实战
git-repo 是一个命令行工具,非常适合应用 sharness 测试框架编写测试用例。参见 [6]。
对于非命令行工具,或者为了测试内置函数,须要先封装一个或多个 fake app,再调用封装的命令行工具进行测试。例如在为 Git 我的项目开发 proc-receive 钩子扩大时,先开发一个 fake app[7]。
之后再编写测试,调用 fake app(test-tool proc-receive),帮忙实现测试用例的开发。参见下列提交中的测试用例 [8]。
还能够应用一些 Shell 编程技巧,在多个测试文件中复用测试用例。例如如下测试用例在测试 HTTP 协定和本地协定时,复用了同一套测试用例(t5411 目录下的测试脚本)[9]。
相干链接
[1]https://www.worldhello.net/2013/10/26/test-gistore-using-git-test-framework.html
[2]https://sourcegraph.com/github.com/git/git@master/-/tree/t
[3]https://github.com/git/git/blob/master/t/t5323-pack-redundant.sh
[4]https://github.com/alibaba/git-repo-go/blob/master/test/t0100-init.sh
[5]https://github.com/git/git/blob/master/Documentation/CodingGuidelines
[6]https://github.com/alibaba/git-repo-go
[7]https://github.com/jiangxin/git/blob/jx/proc-receive-hook/t/helper/test-proc-receive.c
[8]https://github.com/jiangxin/git/commit/9654f5eda1153634ab09ca5c6e490bcabdd57e61
[9]https://github.com/jiangxin/git/blob/jx/proc-receive-hook/t/t5411-proc-receive-hook.sh