Linux中國

利用 BATS 測試 Bash 腳本和庫

用 Java、Ruby 和 Python 等語言編寫應用程序的軟體開發人員擁有複雜的庫,可以幫助他們隨著時間的推移保持軟體的完整性。他們可以創建測試,以在結構化環境中通過執行一系列動作來運行應用程序,以確保其軟體所有的方面均按預期工作。

當這些測試在持續集成(CI)系統中自動進行時,它們的功能就更加強大了,每次推送到源代碼庫都會觸發測試,並且在測試失敗時會立即通知開發人員。這種快速反饋提高了開發人員對其應用程序功能完整性的信心。

Bash 自動測試系統 Bash Automated Testing System BATS)使編寫 Bash 腳本和庫的開發人員能夠將 Java、Ruby、Python 和其他開發人員所使用的相同慣例應用於其 Bash 代碼中。

安裝 BATS

BATS GitHub 頁面包含了安裝指令。有兩個 BATS 輔助庫提供更強大的斷言或允許覆寫 BATS 使用的 Test Anything Protocol(TAP)輸出格式。這些庫可以安裝在一個標準位置,並被所有的腳本引用。更方便的做法是,將 BATS 及其輔助庫的完整版本包含在 Git 倉庫中,用於要測試的每組腳本或庫。這可以通過 git 子模塊 系統來完成。

以下命令會將 BATS 及其輔助庫安裝到 Git 知識庫中的 test 目錄中。

git submodule init
git submodule add https://github.com/sstephenson/bats test/libs/bats
git submodule add https://github.com/ztombol/bats-assert test/libs/bats-assert
git submodule add https://github.com/ztombol/bats-support test/libs/bats-support
git add .
git commit -m 'installed bats'

要克隆 Git 倉庫並同時安裝其子模塊,請在 git clone 時使用 --recurse-submodules 標記。

每個 BATS 測試腳本必須由 bats 可執行文件執行。如果你將 BATS 安裝到源代碼倉庫的 test/libs 目錄中,則可以使用以下命令調用測試:

./test/libs/bats/bin/bats <測試腳本的路徑>

或者,將以下內容添加到每個 BATS 測試腳本的開頭:

#!/usr/bin/env ./test/libs/bats/bin/bats
load &apos;libs/bats-support/load&apos;
load &apos;libs/bats-assert/load&apos;

並且執行命令 chmod +x <測試腳本的路徑>。 這將 a、使它們可與安裝在 ./test/libs/bats 中的 BATS 一同執行,並且 b、包含這些輔助庫。BATS 測試腳本通常存儲在 test 目錄中,並以要測試的腳本命名,擴展名為 .bats。例如,一個測試 bin/build 的 BATS 腳本應稱為 test/build.bats

你還可以通過向 BATS 傳遞正則表達式來運行一整套 BATS 測試文件,例如 ./test/lib/bats/bin/bats test/*.bats

為 BATS 覆蓋率而組織庫和腳本

Bash 腳本和庫必須以一種有效地方式將其內部工作原理暴露給 BATS 進行組織。通常,在調用或執行時庫函數和運行諸多命令的 Shell 腳本不適合進行有效的 BATS 測試。

例如,build.sh 是許多人都會編寫的典型腳本。本質上是一大堆代碼。有些人甚至可能將這堆代碼放入庫中的函數中。但是,在 BATS 測試中運行一大堆代碼,並在單獨的測試用例中覆蓋它可能遇到的所有故障類型是不可能的。測試這堆代碼並有足夠的覆蓋率的唯一方法就是把它分解成許多小的、可重用的、最重要的是可獨立測試的函數。

向庫添加更多的函數很簡單。額外的好處是其中一些函數本身可以變得出奇的有用。將庫函數分解為許多較小的函數後,你可以在 BATS 測試中 援引 source 這些庫,並像測試任何其他命令一樣運行這些函數。

Bash 腳本也必須分解為多個函數,執行腳本時,腳本的主要部分應調用這些函數。此外,還有一個非常有用的技巧,可以讓你更容易地用 BATS 測試 Bash 腳本:將腳本主要部分中執行的所有代碼都移到一個函數中,稱為 run_main。然後,將以下內容添加到腳本的末尾:

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]
then
  run_main
fi

這段額外的代碼做了一些特別的事情。它使腳本在作為腳本執行時與使用 援引 source 進入環境時的行為有所不同。通過援引並測試單個函數,這個技巧使得腳本的測試方式和庫的測試方式變得一樣。例如,這是重構的 build.sh,以獲得更好的 BATS 可測試性

編寫和運行測試

如上所述,BATS 是一個 TAP 兼容的測試框架,其語法和輸出對於使用過其他 TAP 兼容測試套件(例如 JUnit、RSpec 或 Jest)的用戶來說將是熟悉的。它的測試被組織成單個測試腳本。測試腳本被組織成一個或多個描述性 @test 塊中,它們描述了被測試應用程序的單元。每個 @test 塊將運行一系列命令,這些命令準備測試環境、運行要測試的命令,並對被測試命令的退出和輸出進行斷言。許多斷言函數是通過 batsbats-assertbats-support 庫導入的,這些庫在 BATS 測試腳本的開頭載入到環境中。下面是一個典型的 BATS 測試塊:

@test "requires CI_COMMIT_REF_SLUG environment variable" {
  unset CI_COMMIT_REF_SLUG
  assert_empty "${CI_COMMIT_REF_SLUG}"
  run some_command
  assert_failure
  assert_output --partial "CI_COMMIT_REF_SLUG"
}

如果 BATS 腳本包含 setup(安裝)和/或 teardown(拆卸) 函數,則 BATS 將在每個測試塊運行之前和之後自動執行它們。這樣就可以創建環境變數、測試文件以及執行一個或所有測試所需的其他操作,然後在每次測試運行後將其拆卸。Build.bats 是對我們新格式化的 build.sh 腳本的完整 BATS 測試。(此測試中的 mock_docker 命令將在以下關於模擬/打標的部分中進行說明。)

當測試腳本運行時,BATS 使用 exec(執行)來將每個 @test 塊作為單獨的子進程運行。這樣就可以在一個 @test 中導出環境變數甚至函數,而不會影響其他 @test 或污染你當前的 Shell 會話。測試運行的輸出是一種標準格式,可以被人理解,並且可以由 TAP 使用端以編程方式進行解析或操作。下面是 CI_COMMIT_REF_SLUG 測試塊失敗時的輸出示例:

 ✗ requires CI_COMMIT_REF_SLUG environment variable
   (from function `assert_output&apos; in file test/libs/bats-assert/src/assert.bash, line 231,
    in test file test/ci_deploy.bats, line 26)
     `assert_output --partial "CI_COMMIT_REF_SLUG"&apos; failed

   -- output does not contain substring --
   substring (1 lines):
     CI_COMMIT_REF_SLUG
   output (3 lines):
     ./bin/deploy.sh: join_string_by: command not found
     oc error
     Could not login
   --

   ** Did not delete , as test failed **

1 test, 1 failure

下面是成功測試的輸出:

✓ requires CI_COMMIT_REF_SLUG environment variable

輔助庫

像任何 Shell 腳本或庫一樣,BATS 測試腳本可以包括輔助庫,以在測試之間共享通用代碼或增強其性能。這些輔助庫,例如 bats-assertbats-support 甚至可以使用 BATS 進行測試。

庫可以和 BATS 腳本放在同一個測試目錄下,如果測試目錄下的文件數量過多,也可以放在 test/libs 目錄下。BATS 提供了 load 函數,該函數接受一個相對於要測試的腳本的 Bash 文件的路徑(例如,在我們的示例中的 test),並援引該文件。文件必須以後綴 .bash 結尾,但是傳遞給 load 函數的文件路徑不能包含後綴。build.bats 載入 bats-assertbats-support 庫、一個小型 helpers.bash 庫以及 docker_mock.bash 庫(如下所述),以下代碼位於測試腳本的開頭,解釋器魔力行下方:

load &apos;libs/bats-support/load&apos;
load &apos;libs/bats-assert/load&apos;
load &apos;helpers&apos;
load &apos;docker_mock&apos;

打標測試輸入和模擬外部調用

大多數 Bash 腳本和庫運行時都會執行函數和/或可執行文件。通常,它們被編程為基於這些函數或可執行文件的輸出狀態或輸出(stdoutstderr)以特定方式運行。為了正確地測試這些腳本,通常需要製作這些命令的偽版本,這些命令被設計成在特定測試過程中以特定方式運行,稱為「 打標 stubbing 」。可能還需要監視正在測試的程序,以確保其調用了特定命令,或者使用特定參數調用了特定命令,此過程稱為「 模擬 mocking 」。有關更多信息,請查看在 Ruby RSpec 中 有關模擬和打標的討論,它適用於任何測試系統。

Bash shell 提供了一些技巧,可以在你的 BATS 測試腳本中使用這些技巧進行模擬和打標。所有這些都需要使用帶有 -f 標誌的 Bash export 命令來導出一個覆蓋了原始函數或可執行文件的函數。必須在測試程序執行之前完成此操作。下面是重寫可執行命令 cat 的簡單示例:

function cat() { echo "THIS WOULD CAT ${*}" }
export -f cat

此方法以相同的方式覆蓋了函數。如果一個測試需要覆蓋要測試的腳本或庫中的函數,則在對函數進行打標或模擬之前,必須先聲明已測試腳本或庫,這一點很重要。否則,在聲明腳本時,打標/模擬將被原函數替代。另外,在運行即將進行的測試命令之前確認打標/模擬。下面是build.bats 的示例,該示例模擬 build.sh 中描述的raise 函數,以確保登錄函數會引發特定的錯誤消息:

@test ".login raises on oc error" {
  source ${profile_script}
  function raise() { echo "${1} raised"; }
  export -f raise
  run login
  assert_failure
  assert_output -p "Could not login raised"
}

一般情況下,沒有必要在測試後復原打標/模擬的函數,因為 export(輸出)僅在當前 @test 塊的 exec(執行)期間影響當前子進程。但是,可以模擬/打標 BATS assert 函數在內部使用的命令(例如 catsed 等)是可能的。在運行這些斷言命令之前,必須對這些模擬/打標函數進行 unset(復原),否則它們將無法正常工作。下面是 build.bats 中的一個示例,該示例模擬 sed,運行 build_deployable 函數並在運行任何斷言之前復原 sed

@test ".build_deployable prints information, runs docker build on a modified Dockerfile.production and publish_image when its not a dry_run" {
  local expected_dockerfile=&apos;Dockerfile.production&apos;
  local application=&apos;application&apos;
  local environment=&apos;environment&apos;
  local expected_original_base_image="${application}"
  local expected_candidate_image="${application}-candidate:${environment}"
  local expected_deployable_image="${application}:${environment}"
  source ${profile_script}
  mock_docker build --build-arg OAUTH_CLIENT_ID --build-arg OAUTH_REDIRECT --build-arg DDS_API_BASE_URL -t "${expected_deployable_image}" -
  function publish_image() { echo "publish_image ${*}"; }
  export -f publish_image
  function sed() {
    echo "sed ${*}" >&2;
    echo "FROM application-candidate:environment";
  }
  export -f sed
  run build_deployable "${application}" "${environment}"
  assert_success
  unset sed
  assert_output --regexp "sed.*${expected_dockerfile}"
  assert_output -p "Building ${expected_original_base_image} deployable ${expected_deployable_image} FROM ${expected_candidate_image}"
  assert_output -p "FROM ${expected_candidate_image} piped"
  assert_output -p "build --build-arg OAUTH_CLIENT_ID --build-arg OAUTH_REDIRECT --build-arg DDS_API_BASE_URL -t ${expected_deployable_image} -"
  assert_output -p "publish_image ${expected_deployable_image}"
}

有的時候相同的命令,例如 foo,將在被測試的同一函數中使用不同的參數多次調用。這些情況需要創建一組函數:

  • mock_foo:將期望的參數作為輸入,並將其持久化到 TMP 文件中
  • foo:命令的模擬版本,該命令使用持久化的預期參數列表處理每個調用。必須使用 export -f 將其導出。
  • cleanup_foo:刪除 TMP 文件,用於拆卸函數。這可以進行測試以確保在刪除之前成功完成 @test 塊。

由於此功能通常在不同的測試中重複使用,因此創建一個可以像其他庫一樣載入的輔助庫會變得有意義。

docker_mock.bash 是一個很棒的例子。它被載入到 build.bats 中,並在任何測試調用 Docker 可執行文件的函數的測試塊中使用。使用 docker_mock 典型的測試塊如下所示:

@test ".publish_image fails if docker push fails" {
  setup_publish
  local expected_image="image"
  local expected_publishable_image="${CI_REGISTRY_IMAGE}/${expected_image}"
  source ${profile_script}
  mock_docker tag "${expected_image}" "${expected_publishable_image}"
  mock_docker push "${expected_publishable_image}" and_fail
  run publish_image "${expected_image}"
  assert_failure
  assert_output -p "tagging ${expected_image} as ${expected_publishable_image}"
  assert_output -p "tag ${expected_image} ${expected_publishable_image}"
  assert_output -p "pushing image to gitlab registry"
  assert_output -p "push ${expected_publishable_image}"
}

該測試建立了一個使用不同的參數兩次調用 Docker 的預期。在對Docker 的第二次調用失敗時,它會運行測試命令,然後測試退出狀態和對 Docker 調用的預期。

一方面 BATS 利用 mock_docker.bash 引入 ${BATS_TMPDIR} 環境變數,BATS 在測試開始的位置對其進行了設置,以允許測試和輔助程序在標準位置創建和銷毀 TMP 文件。如果測試失敗,mock_docker.bash 庫不會刪除其持久化的模擬文件,但會列印出其所在位置,以便可以查看和刪除它。你可能需要定期從該目錄中清除舊的模擬文件。

關於模擬/打標的一個注意事項:build.bats 測試有意識地違反了關於測試聲明的規定:不要模擬沒有擁有的! 該規定要求調用開發人員沒有編寫代碼的測試命令,例如 dockercatsed 等,應封裝在自己的庫中,應在使用它們腳本的測試中對其進行模擬。然後應該在不模擬外部命令的情況下測試封裝庫。

這是一個很好的建議,而忽略它是有代價的。如果 Docker CLI API 發生變化,則測試腳本不會檢測到此變化,從而導致錯誤內容直到經過測試的 build.sh 腳本在使用新版本 Docker 的生產環境中運行後才顯示出來。測試開發人員必須確定要嚴格遵守此標準的程度,但是他們應該了解其所涉及的權衡。

總結

在任何軟體開發項目中引入測試製度,都會在以下兩方面產生權衡: a、增加開發和維護代碼及測試所需的時間和組織,b、增加開發人員在對應用程序整個生命周期中完整性的信心。測試製度可能不適用於所有腳本和庫。

通常,滿足以下一個或多個條件的腳本和庫才可以使用 BATS 測試:

  • 值得存儲在源代碼管理中
  • 用於關鍵流程中,並依靠它們長期穩定運行
  • 需要定期對其進行修改以添加/刪除/修改其功能
  • 可以被其他人使用

一旦決定將測試規則應用於一個或多個 Bash 腳本或庫,BATS 就提供其他軟體開發環境中可用的全面測試功能。

致謝:感謝 Darrin Mann 向我引薦了 BATS 測試。

via: https://opensource.com/article/19/2/testing-bash-bats

作者:Darin London 選題:lujun9972 譯者:stevenzdg988 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國