Skip to content

Commit 2a703a1

Browse files
committed
New post: test gistore using git test framework
Signed-off-by: Jiang Xin <[email protected]>
1 parent 8642b4f commit 2a703a1

File tree

1 file changed

+297
-0
lines changed

1 file changed

+297
-0
lines changed
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
---
2+
layout: post
3+
title: "复用 git.git 测试框架"
4+
---
5+
6+
Git 项目(git.git)有着别具一格的测试框架,使用 shell 脚本开发测试用例,
7+
写起测试用例来一点都感觉不到拖泥带水,就和在 shell 环境中手工测试一样。
8+
最近在重构 Gistore 项目时复用了这一 Git 项目特有的测试框架,对 Gistore
9+
进行测试。愿这一测试框架可以被更多的项目借鉴。
10+
11+
## git.git 的测试框架 ##
12+
13+
Git 项目主要采用了 C 语言,同时还包含了 Perl、Shell 等多种开发语言的项目。
14+
Git 项目的测试并没有采用常见的类似 JUnit 测试框架,而是采用自创的测试框架,
15+
由 Junio Hamano 在 2005 年用 shell 脚本封装而成。在这个框架下,
16+
写测试用例和测试套件自然也是使用 shell 脚本语言,写起测试用例来就和手工在
17+
shell 环境下针对命令行测试没什么两样,写测试用例的过程很是“享受”。还一个原因可能是
18+
shell 脚本语言几乎融入了每一个 \*nix 开发者的血液中。总之这个测试框架用起来非常顺手。
19+
20+
在 Git 项目的 `t/` 目录下存在成百上千个以 "`t<四位数字>-<测试套件名称>.sh`"
21+
格式命名的文件。每一个 Shell 脚本文件即是一个测试套件,其中包含多个测试用例。
22+
23+
若打开这些 shell 脚本,会注意到每一个测试套件(`t<四位数字>-<套件名>.sh`)都包含相似的结构。
24+
25+
# 引入测试套件函数库
26+
. ./test-lib.sh
27+
28+
# 定义和执行一个测试用例
29+
test_expect_success '<测试用例名称>' '
30+
<测试断言1> &&
31+
<测试断言2> &&
32+
...
33+
<测试断言n>
34+
'
35+
36+
# 此处省略更多的测试用例
37+
test_expect_success ...
38+
39+
...
40+
41+
# 声明测试套件结束,并对测试执行过程为测试套件生成的临时目录进行清理
42+
test_done
43+
44+
这些 shell 脚本(测试套件)都可以单独运行。例如下面示例中执行的测试套件就是我为
45+
git-clean--interactive (交互式 git clean)写的测试套件。
46+
47+
$ sh t7301-clean-interactive.sh
48+
ok 1 - setup
49+
ok 2 - git clean -i (c: clean hotkey)
50+
ok 3 - git clean -i (cl: clean prefix)
51+
ok 4 - git clean -i (quit)
52+
ok 5 - git clean -i (Ctrl+D)
53+
ok 6 - git clean -id (filter all)
54+
ok 7 - git clean -id (filter patterns)
55+
ok 8 - git clean -id (filter patterns 2)
56+
ok 9 - git clean -id (select - all)
57+
ok 10 - git clean -id (select - none)
58+
ok 11 - git clean -id (select - number)
59+
ok 12 - git clean -id (select - number 2)
60+
ok 13 - git clean -id (select - number 3)
61+
ok 14 - git clean -id (select - filenames)
62+
ok 15 - git clean -id (select - range)
63+
ok 16 - git clean -id (select - range 2)
64+
ok 17 - git clean -id (inverse select)
65+
ok 18 - git clean -id (ask)
66+
ok 19 - git clean -id (ask - Ctrl+D)
67+
ok 20 - git clean -id with prefix and path (filter)
68+
ok 21 - git clean -id with prefix and path (select by name)
69+
ok 22 - git clean -id with prefix and path (ask)
70+
# passed all 22 test(s)
71+
1..22
72+
73+
运行测试套件的输出结果(显示到标准输出的内容)是经过特别设计的。成功运行的测试用例显示为:
74+
75+
ok <数字> - <测试用例名>
76+
77+
而运行失败的测试用例会显示为:
78+
79+
not ok <数字> - <测试用例名>
80+
81+
在测试套件运行的结尾会显示如下统计信息:
82+
83+
<数字>..<数字>
84+
85+
这种特定的输出格式被称为 TAP (Test Anything Protocol),参见 <http://testanything.org/>
86+
87+
Junio 还用 shell 脚本封装了一个测试夹具(test harness),在 `t/` 目录下,直接执行 `make`
88+
命令即可执行全部的测试套件,并对测试结果进行统计。此外还有其他的测试夹具可供使用,
89+
例如名为 `prove` 的命令可以多进程并发地执行测试套件,让测试过程更高效。
90+
91+
$ prove --timer --jobs 15 ./t[0-9]*.sh
92+
[19:17:33] ./t0005-signals.sh ................................... ok 36 ms
93+
[19:17:33] ./t0022-crlf-rename.sh ............................... ok 69 ms
94+
[19:17:33] ./t0024-crlf-archive.sh .............................. ok 154 ms
95+
[19:17:33] ./t0004-unwritable.sh ................................ ok 289 ms
96+
[19:17:33] ./t0002-gitfile.sh ................................... ok 480 ms
97+
===( 102;0 25/? 6/? 5/? 16/? 1/? 4/? 2/? 1/? 3/? 1... )===
98+
99+
100+
## 测试 Gistore ##
101+
102+
[Gistore](https://github.com/jiangxin/gistore/) 是我在2010年写的一个工具,
103+
以 Git 作为后端存储实现对磁盘文件的备份,并作为独立的一章写到了《Git权威指南》
104+
一书中。
105+
106+
Gistore = Git + Store
107+
108+
最近用 Ruby 语言重写了 Gistore。这是因为 Gistore 最初的设计依赖 mount 命令,
109+
需要将备份目录挂载到临时工作区,故只能用于有限的平台上,且可能需要 root 用户权限。
110+
考虑到 Git 的 gitignore 语法增加了对双星号(**)通配符的支持,是不是用
111+
gitignore 机制实现 Gistore 更好呢?改用 Ruby 实现是因为最近几年 Ruby 用得多,
112+
而且使用 Thor (一个实现命令行编程框架的 Ruby 包,被很多著名软件如 bundle、rails
113+
等使用)可以更容易实现工具的命令行扩展。
114+
115+
软件重构的质量需要测试用例来保证。Ruby 虽然内置了强大的测试框架,但像 Gistore
116+
这类大量调用外部命令的应用,采用 Git 项目的测试框架可能更理想。于是在我 Gistore
117+
项目中重用了 Git 项目的测试框架。
118+
119+
使用该测试框架的注意事项如下:
120+
121+
122+
### 用 && 组合多个测试断言 ###
123+
124+
下面的测试用例中,因为在第二句断言(false)后面丢掉了一个 && ,
125+
导致前两个断言未对测试用例施加影响。
126+
127+
#!/bin/sh
128+
#
129+
130+
. ./test-lib.sh
131+
132+
test_expect_success 'test framework assertion' '
133+
true &&
134+
false
135+
true
136+
'
137+
138+
test_done
139+
140+
### 用 test_cmp 断言测试输出 ###
141+
142+
该测试框架中最常用到的断言除了 shell 本身包含的 `test` 命令外,就是 `test_cmp` 断言。
143+
实际上 `test_cmp` 就是对 `diff` 命令的简单封装。具体的使用过程是先将预期结果写入文件
144+
`expect` ,测试输出写入 `actual` 文件,再用 `test_cmp` 比较 `expect``actual` 文件,
145+
内容一致则成功,否则失败。例如下面的测试用例代码:
146+
147+
cat >expect << EOF
148+
root/doc/COPYRIGHT
149+
root/src/README.txt
150+
root/src/images/test-binary-1.png
151+
root/src/images/test-binary-2.png
152+
root/src/lib/a/foo.c
153+
root/src/lib/b/bar.o
154+
root/src/lib/b/baz.a
155+
EOF
156+
157+
test_expect_success 'initialize for commit' '
158+
prepare_work_tree &&
159+
gistore init --repo repo.git &&
160+
gistore add --repo repo.git root/src &&
161+
gistore add --repo repo.git root/doc &&
162+
gistore commit --repo repo.git &&
163+
test "$(count_git_commits repo.git)" = "1" &&
164+
gistore repo repo.git ls-tree --name-only \
165+
-r HEAD | sed -e "s#^${cwd#/}/##g" > actual &&
166+
test_cmp expect actual
167+
'
168+
169+
### 用 test_must_fail 断言命令失败或异常 ###
170+
171+
该测试框架中有两个看起来很像的方法 `test_expect_failure``test_must_fail`
172+
前一个函数类似于 `test_expect_success`,以命令参数的方式引入一个测试用例并进行测试。
173+
后一个是用于测试用例中的测试断言。
174+
175+
函数 `test_expect_failure` 通过命令行参数引入的测试用例,无论执行成功与否,
176+
测试都不会中断。测试用例执行失败会显示:
177+
178+
not ok 1 - test framework assertion # TODO known breakage
179+
# still have 1 known breakage(s)
180+
181+
测试成功会显示:
182+
183+
ok 1 - test framework assertion # TODO known breakage vanished
184+
# 1 known breakage(s) vanished; please update test(s)
185+
186+
函数 `test_must_fail` 作为测试断言,用于确认一个命令会以失败结束(返回非0值)。
187+
例如下面测试用例用于测试对所有注册的备份任务执行备份时(即执行 `gistore commit-all`
188+
命令时),如果有一个或多个 Gistore 备份任务的指向丢失时,其它备份任务的备份不会受到影响,
189+
并且 `gistore commit-all` 命令运行结束后要返回非零值。
190+
191+
test_expect_success 'commit-all while missing task repo' '
192+
gistore task add hello repo1.git &&
193+
gistore task add world repo2.git &&
194+
test "$(count_git_commits repo1.git)" = "4" &&
195+
test "$(count_git_commits repo2.git)" = "3" &&
196+
do_hack &&
197+
gistore commit-all &&
198+
test "$(count_git_commits repo1.git)" = "5" &&
199+
test "$(count_git_commits repo2.git)" = "4" &&
200+
mv repo1.git repo1.git.moved &&
201+
do_hack &&
202+
test_must_fail gistore commit-all &&
203+
test "$(count_git_commits repo2.git)" = "5" &&
204+
mv repo1.git.moved repo1.git &&
205+
mv repo2.git repo2.git.moved &&
206+
test_must_fail gistore commit-all &&
207+
test "$(count_git_commits repo1.git)" = "6"
208+
'
209+
210+
### 测试用例设置依赖条件按需运行 ###
211+
212+
因为 Git 1.8.2 之后才为 gitignore 引入双星号(\*\*)通配符,而之前版本的 Git 并不支持,
213+
这会导致某些测试用例结果不一致。
214+
215+
Git项目的测试框架在设计之初就考虑到了这种情况,可以通过设置依赖条件在某些情况下关闭特定测试用例的运行。
216+
217+
首先在测试框架中根据执行环境的不同,预置特定的依赖条件,例如下面的代码使得当 Git 命令的版本是
218+
1.8.2 或更新的版本时,预置 `GIT_CAP_WILDMATCH` 依赖条件。
219+
220+
if test $(gistore check-git-version 1.8.2) -ge 0; then
221+
test_set_prereq GIT_CAP_WILDMATCH
222+
fi
223+
224+
然后在定义测试用例的 `test_expect_success` 的第一个参数中写入相应的依赖条件。
225+
例如如下的测试用例只在 Git 1.8.2 以上环境下运行。
226+
227+
228+
# Before git v1.7.4, filenames in git-status are NOT quoted.
229+
# So strip double quote before compare with this.
230+
cat >expect << EOF
231+
M root/doc/COPYRIGHT
232+
M root/src/README.txt
233+
D root/src/images/test-binary-1.png
234+
D root/src/lib/b/baz.a
235+
?? root/src/lib/a/foo.h
236+
EOF
237+
238+
test_expect_success GIT_CAP_WILDMATCH 'status --git (1)' '
239+
gistore commit --repo repo.git && \
240+
echo "hack" >> root/doc/COPYRIGHT && \
241+
echo "hack" >> root/src/README.txt && \
242+
touch root/src/lib/a/foo.h && \
243+
rm root/src/images/test-binary-1.png && \
244+
rm root/src/lib/b/baz.a && \
245+
gistore status --repo repo.git --git -s \
246+
| sed -e "s#${cwd#/}/##g" | sed -e "s/\"//g" > actual &&
247+
test_cmp expect actual
248+
'
249+
250+
### 进行函数级测试 ###
251+
252+
Git项目的测试框架主要是进行集成测试,如果需要进行函数级测试,还需要下点功夫。
253+
即需要对函数进行简单的命令行封装,用命令行调用的方式对函数进行测试。
254+
255+
在 Git 项目中就用代码 "`test-path-utils.c`" 对路径处理相关函数进行封装,在测试用例 `t0060`
256+
中调用 `test-path-utils` 进行相关测试。
257+
258+
. ./test-lib.sh
259+
260+
relative_path() {
261+
expected=$(test-path-utils print_path "$3")
262+
test_expect_success $4 "relative path: $1 $2 => $3" \
263+
"test \"\$(test-path-utils relative_path '$1' '$2')\" = '$expected'"
264+
}
265+
266+
relative_path /foo/a/b/c/ /foo/a/b/ c/
267+
relative_path /foo/a/b/c/ /foo/a/b c/
268+
relative_path /foo/a//b//c/ ///foo/a/b// c/ POSIX
269+
relative_path /foo/a/b /foo/a/b ./
270+
relative_path /foo/a/b/ /foo/a/b ./
271+
relative_path /foo/a /foo/a/b ../
272+
relative_path / /foo/a/b/ ../../../
273+
274+
275+
在 Gistore 项目中,我也用到了类似的方法。通过隐含子命令 `check-git-version`
276+
`Gistore.git_version_compare` 方法进行封装,并在测试用例 `t0020` 中进行针对性测试。
277+
278+
test_expect_success 'compare two versions' '
279+
test $(gistore check-git-version 1.8.5 1.8.5) -eq 0 &&
280+
test $(gistore check-git-version 1.8.4 1.8.4.1) -eq -1 &&
281+
test $(gistore check-git-version 1.7.5 1.7.11) -eq -1 &&
282+
test $(gistore check-git-version 1.7.11 1.7.5) -eq 1 &&
283+
test $(gistore check-git-version 1.7.11 1.7.5) -eq 1 &&
284+
test $(gistore check-git-version 1.7.11 2.0) -eq -1 &&
285+
test $(gistore check-git-version 2.0 1.8.5) -eq 1
286+
'
287+
288+
更多测试用例的写法,参见如下链接:
289+
290+
* [Git 项目中的测试用例的说明文件](https://github.com/git/git/blob/master/t/README)
291+
* [Git 项目中的测试用例](https://github.com/git/git/tree/master/t)
292+
* [Gistore 项目中的测试用例](https://github.com/jiangxin/gistore/tree/master/t)
293+
294+
插播小广告:
295+
296+
* [学习Git,读《Git权威指南》](http://www.worldhello.net/gotgit/bookstore.html)
297+
* [Gistore: 以Git为后端的备份解决方案](https://github.com/jiangxin/gistore)

0 commit comments

Comments
 (0)