测试用例难写?来试试 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 项目测试框架的结构。

测试用例难写?来试试 Sharness

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

测试用例难写?来试试 Sharness

  • 待测应用放在项目的根目录。
  • 测试脚本命名为 .t,即扩展名为 .t 的脚本文件。
  • 每一个测试用例在执行时会创建一个独立的临时目录,例如 trash directory.simple.t。测试用例执行成功,则该目录会被删除。
  • 在 sharness.d 目录下添加自定义脚本,可以扩展 Sharness 框架。即在框架代码加载时,自动加载该目录下文件。

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

  • 定制版本对测试框架代码做了进一步封装,框架代码放在单独的子目录。
  • 测试脚本的名称恢复为和 Git 项目测试脚本类似的名称(tNNNN-.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 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

上一篇:5G专网为“江南皮革厂”带来了什么?


下一篇:专为国内开发者定制的Spark电子书来了!