天天看點

測試用例難寫?來試試 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 項目的測試架構抽象而來,項目位址:

測試用例難寫?來試試 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"           
. ./lib/sharness.sh           
# Create manifest repositories 
 manifest_url="file://${REPO_TEST_REPOSITORIES}/hello/manifests"           
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
     )
 '           
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