测试用例难写?来试试 Sharness

2020 年 8 月 24 日 阿里技术

阿里妹导读:Sharness 是一个用 Shell 脚本来编写测试用例的测试框架。本文将详细介绍 Sharness 的结构及测试用例的编写格式,以及语法规范和技巧,教大家如何使用 Sharness 编写测试用例,同时还分享了 Sharness 的扩展功能和项目实战


文末福利:Hologres训练营,专家带练,玩转实时数仓。


参与过 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 <<-EOFHelloworld.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_pausetest_done

然后使用 -v 参数运行该脚本,会在 test_pause 语句处中断,进入一个包含 sharness 环境变量的子 Shell 中,目录会切换到测试用例单独的工作区。调试完毕退出 Shell 即返回。

三  Git 项目的测试框架结构

Sharness 源自于 Git 项目的测试用例框架。我们先来看看 Git 项目测试框架的结构。


Git 项目测试相关文件

  • 待测应用放在项目的根目录。例如 Git 项目的待测应用: git 和 git-receive-pack 等。


  • 测试框架修改 PATH 环境变量,使得测试用例在调用待测应用(如 git 命令)的时候,优先使用项目根目录下的待测应用。


  • 测试脚本命名为 tNNNN-<name>.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


Sharness 测试框架示例

  • 待测应用放在项目的根目录。


  • 测试脚本命名为 <name>.t,即扩展名为 .t 的脚本文件。


  • 每一个测试用例在执行时会创建一个独立的临时目录,例如 trash directory.simple.t。测试用例执行成功,则该目录会被删除。


  • sharness.d 目录下添加自定义脚本,可以扩展 Sharness 框架。即在框架代码加载时,自动加载该目录下文件。


我们对 Sharness 测试框架做了一些小改动:

  • 定制版本对测试框架代码做了进一步封装,框架代码放在单独的子目录。


  • 测试脚本的名称恢复为和 Git 项目测试脚本类似的名称(tNNNN-<name>.sh), 即以字母 t 和四位数字开头的脚本文件。


定制版 Sharness 测试框架示例

六  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 totransform its input or what its output is supposed to look like. Itdoes 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 quotedthe end-of-here-text marker here, i.e. "<<\-EOF"? That way, you canlose 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_DATEGIT_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
 


实时数仓Hologres训练营

技术专家带练


5天打卡实时数仓Hologres训练营,赢限量T恤。5 位领域专家带你从HSAP与Hologres设计原理到上手实操,轻松玩转实时数仓。仅余600席!先抢先赢。


点击“阅读原文”,立即报名参加吧!      


关注「阿里技术」
把握前沿技术脉搏


戳我,去训练营。
登录查看更多
0

相关内容

Git 是一个为了更好地管理 Linux 内核开发而创立的分布式版本控制和软件配置管理软件。 国内外知名 Git 代码托管网站有: GitHub.com Coding.net code.csdn.net ...
专知会员服务
173+阅读 · 2020年6月4日
【干货书】用于概率、统计和机器学习的Python,288页pdf
专知会员服务
288+阅读 · 2020年6月3日
【Manning新书】现代Java实战,592页pdf
专知会员服务
99+阅读 · 2020年5月22日
Python导论,476页pdf,现代Python计算
专知会员服务
260+阅读 · 2020年5月17日
【GitHub】BERT模型从训练到部署全流程
专知
34+阅读 · 2019年6月28日
Kali Linux 渗透测试:密码攻击
计算机与网络安全
16+阅读 · 2019年5月13日
使用 C# 和 Blazor 进行全栈开发
DotNet
6+阅读 · 2019年4月15日
你真的会正确地调试 TensorFlow 代码吗?
数据库开发
7+阅读 · 2019年3月18日
百度开源项目OpenRASP快速上手指南
黑客技术与网络安全
5+阅读 · 2019年2月12日
已删除
将门创投
4+阅读 · 2018年12月10日
机器翻译不可不知的 Seq2Seq 模型
AI研习社
4+阅读 · 2018年5月24日
A Survey of Deep Learning for Scientific Discovery
Arxiv
29+阅读 · 2020年3月26日
Deep learning for cardiac image segmentation: A review
Arxiv
21+阅读 · 2019年11月9日
Arxiv
5+阅读 · 2018年12月18日
Arxiv
151+阅读 · 2017年8月1日
VIP会员
相关资讯
【GitHub】BERT模型从训练到部署全流程
专知
34+阅读 · 2019年6月28日
Kali Linux 渗透测试:密码攻击
计算机与网络安全
16+阅读 · 2019年5月13日
使用 C# 和 Blazor 进行全栈开发
DotNet
6+阅读 · 2019年4月15日
你真的会正确地调试 TensorFlow 代码吗?
数据库开发
7+阅读 · 2019年3月18日
百度开源项目OpenRASP快速上手指南
黑客技术与网络安全
5+阅读 · 2019年2月12日
已删除
将门创投
4+阅读 · 2018年12月10日
机器翻译不可不知的 Seq2Seq 模型
AI研习社
4+阅读 · 2018年5月24日
Top
微信扫码咨询专知VIP会员