测试用例难写?来试试 Sharness

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

image.png
参与过 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 项目测试框架的结构。

image.png

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

image.png

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

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

  • 定制版本对测试框架代码做了进一步封装,框架代码放在单独的子目录。
  • 测试脚本的名称恢复为和 Git 项目测试脚本类似的名称(tNNNN-.sh), 即以字母 t 和四位数字开头的脚本文件。

image.png

六 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

目录
相关文章
|
Web App开发 自然语言处理 监控
基于 WebAssembly 的AIoT应用框架实践
天猫精灵大前端团队基于 WebAssembly 的AIoT应用框架实践分享。
基于 WebAssembly 的AIoT应用框架实践
|
7月前
|
网络安全 文件存储 数据安全/隐私保护
路由器配置 DDNS 实现稳定的远程访问
本文详解家庭内网穿透的极简方案:只需将光猫拨号改为路由器PPPoE拨号,获取公网IP,再搭配DDNS(如花生壳、Cloudflare)绑定固定域名,最后配置端口转发。无需服务器、不依赖中转,零成本、高稳定,轻松实现NAS远程访问、远程桌面、自建服务外网共享等实用场景。
2181 5
|
9月前
|
机器学习/深度学习 人工智能 自然语言处理
构建AI智能体:六十一、信息论完全指南:从基础概念到在大模型中的实际应用
摘要: 信息论是人工智能尤其是大语言模型的核心数学工具。本文系统介绍了八大核心概念: 信息量:衡量事件意外程度,公式为I(x)=-log₂P(x) 信息熵:评估系统不确定性,H(X)=-ΣP(x)log₂P(x) 联合熵/条件熵:分析多变量关系及条件不确定性 互信息:量化变量间共享信息量 KL散度:衡量概率分布差异 交叉熵:模型训练的核心损失函数 在大语言模型中,这些概念被广泛应用于: 训练阶段:交叉熵优化预测,KL散度防止过拟合 推理阶段:温度参数调节生成文本的创造性(高熵增加多样性)
1093 2
|
5月前
|
人工智能 运维 分布式计算
算力云端化:数字时代的算力革命——核心优点与多元应用
在数字经济时代,算力成为第五大生产要素。算力云端化以“算力即服务”模式,突破硬件、地域与运维限制,具备降本、弹性、易用、协同、共享、国产适配六大优势,已深度赋能工业、科研、医疗、AI、教育等数十个领域,加速千行百业数字化转型。(239字)
|
6月前
|
人工智能 安全 API
OpenClaw部署全指南 阿里云秒级上线+本地多系统搭建+企业内网安全加固+大模型API配置
OpenClaw(原Clawdbot)作为轻量级开源AI智能体执行框架,凭借自然语言驱动、多工具协同、技能可扩展的特性,已成为2026年政企办公、自动化运维、知识管理场景的核心工具。其部署形态可灵活适配公有云、本地单机、企业内网等不同环境,既可以通过阿里云实现秒级自动化部署,快速对接云端大模型实现轻量化应用,也能在MacOS、Linux、Windows11本地环境完成私有化搭建,更可针对政企涉密场景实现纯内网离线部署,满足等保合规、数据不出域的安全要求。本文将完整覆盖2026年OpenClaw的全场景部署流程,包括阿里云计算巢秒级部署、本地三大系统基础部署、阿里云千问大模型与免费Coding
787 1
|
8月前
|
缓存 JSON 数据安全/隐私保护
58 同城 item_get - 获取详情数据接口对接全攻略:从入门到精通
58同城item_get接口通过item_id获取房产、招聘、二手车等本地生活信息详情,支持HTTPS+签名认证、JSON/XML返回,具备实时性强、字段丰富、权限分级等特点。本指南涵盖接口认知、权限申请、开发对接、调试优化及生产级最佳实践,助力构建本地生活服务平台与数据中台。
|
8月前
|
人工智能 前端开发 API
智能体来了:从0到1:真正的第一步,不是调用API
本文厘清智能体本质:它不是大模型本身,而是以LLM为中枢、具备感知、规划、记忆与工具调用的闭环系统。强调“真正第一步”是构建可失败、可反馈的任务工作流,而非堆砌提示词;主张从垂直场景出发,以业务逻辑为核心,借力平台化工具落地。
408 0
|
12月前
|
NoSQL API 双11
淘宝订单 API 实战:90% 开发者会踩的 “漏单坑”,我用这 3 招彻底解决
本文深入解析了淘宝订单 API 对接中常见的“漏单”问题,结合实战案例拆解了同步逻辑、漏单原因及解决方案。重点分析了“主动轮询”与“回调通知”两种方式的优缺点,指出高频漏单的三大原因:回调丢包、时间窗口设置错误、订单状态判断不全,并提供对应的解决策略。最后分享了一套“回调 + 轮询”双保险方案,帮助彻底杜绝漏单问题。
|
算法 Java 数据库
软件系统授权方案设计
软件系统授权方案设计
918 60
|
存储 运维 监控
为什么网络日志如此重要?
日志审计是网络安全的重要组成部分,通过分析网络日志,可快速定位故障、解决危机并提升系统安全性。网络日志记录了文件访问、用户登录等详细信息,甚至受某些法规约束需包含额外数据。日志审计无法被绕过,其在检测安全漏洞、法律取证和员工行为监控中作用显著。推荐一款工具EventLogAnalyzer,具备日志采集、分类存储、事件报警等功能,满足企业需求。购买时需注意品牌、功能及试用期,以选择最适合的产品。
625 11