在正式的場景,代碼寫完後都是須要測試的,shell 腳本也不例外。但 shell 腳本的特性致使測試方法和其餘語言有所不一樣。git
做爲一種重要的測試方法,單元測試在不少種編程語言程序測試中起到舉重輕重的做用。但不幸的是,單元測試基本不適用於 shell 腳本。並非說 shell 腳本不能被單元測試,而是說單元測試能測試出來的問題不多,投入卻很大。爲了讓 shell 腳本能被單元測試,50 行的代碼極可能要改寫成 100 多行甚至更多行。更重要的是 shell 腳本嚴重依賴外部環境,多數問題須要對腳本總體進行功能測試才能發現,而不是對單個函數進行單元測試。對單元測試的精力投入極可能會減小在功能測試的精力投入。github
因此不建議推行 shell 腳本的單元測試,這不只會讓開發者很痛苦,也很難減小問題的出現概率,甚至有可能拔苗助長。shell
Shell 腳本的最小測試粒度是單個腳本。必須保證單個腳本是容易測試的,不能多個腳本耦合太緊密而難以對其中某一個進行單獨測試。數據庫
有主體邏輯的腳本依賴的外部環境必須是容易模擬的。好比須要從數據庫中讀取數據,對數據進行處理,而後寫入到文件中,這些功能不能在同一個腳本中完成。由於數據庫這個外部環境不容易模擬,會致使測試困難。須要把讀寫數據庫的功能獨立成單獨的腳本,功能儘可能簡單,測試該腳本時只須要關心數據是否正常讀取了出來,格式是否被正確轉換等等,而不須要關心處理數據的具體邏輯。處理數據的主體邏輯代碼要獨立成一個(或者多個)腳本,測試該腳本時,無需準備數據庫環境,直接用另外一個腳本或者數據文件取代讀取數據庫的腳本,提供測試數據。若是文件寫入的環境複雜(好比文件或者目錄結構複雜,或者要寫入到分佈式文件系統等等),也須要將文件寫入的腳本獨立出來以便更易於測試。編程
對有主體邏輯的腳本進行功能測試,不能手動進行,必須寫測試腳本,能夠自動運行。每次腳本改動後進行迴歸測試。項目穩定後,能夠在每次提交代碼後自動運行測試腳本。測試腳本必須覆蓋正常和異常狀況,不能只覆蓋正常狀況。異常狀況的多少,要根據腳本的複雜度而定。微信
有複雜外部依賴的腳本,功能必須單一,邏輯儘可能簡單,代碼儘可能穩定,不常常改動。好比讀寫數據庫、啓停進程、複雜的目錄文件操做等有複雜外部依賴的腳本,功能必須單一,只與一個特定的外部依賴交互,提供儘可能和外部依賴無關的中間數據,儘可能不包含和外部環境無關的邏輯。該類腳本要容易模擬,以便在測試其餘部分時再也不須要依賴外部環境。編程語言
對於有複雜外部依賴的腳本,能夠寫腳本自動測試,也能夠手動測試,測試時須要包含正常和異常的狀況,不能只測試正常狀況。分佈式
須要寫腳本完成以下功能:ide
若是 process1 和 process2 兩個進程都存在,以 process2 進程 cwd 目錄中的 data/output.txt
爲輸入,作一些比較複雜的處理,而後輸出到 process1 進程 cwd 目錄中的 data/input.txt
文件(若是該文件已存在,則不處理),處理完後,刪除以前的 data/output.txt
。函數
分析:
process1 和 process2 兩個進程都是複雜的外部依賴,不能在主體邏輯腳本里直接依賴它們,因此要把檢查進程是否存在的邏輯獨立成單獨的腳本。輸入和輸出文件的路徑依賴進程路徑,爲了測試方便,也要把獲取文件路徑的邏輯獨立成單獨的腳本。
腳本功能實現:
檢查進程是否存在和獲取進程 cwd 目錄的 util.zsh 腳本:
#!/bin/zsh check_process() { pidof $1 } get_process_cwd() { readlink /proc/$1/cwd }
主體邏輯腳本 main.zsh:
#!/bin/zsh # 有錯誤即退出,能夠省掉不少錯誤處理的代碼 set -e # 切換到腳本當前目錄 cd ${0:h} # 加載依賴的腳本 source ./util.zsh # 檢查進程是否存在 local process1_pid=$(check_process process1) local process2_pid=$(check_process process2) # 這裏的 input 和 output 是相對腳原本說的 local input_file=$(get_process_cwd $process2_pid)/data/output.txt local output_file=$(get_process_cwd $process1_pid)/data/input.txt # 若是輸入文件不存在,直接退出 [[ -e $input_file ]] || { echo $input_file not found. exit 1 } # 若是輸出文件已存在,也直接退出 [[ -e $output_file ]] && { echo $output_file already exists. exit 0 } # 處理 $input_file 內容 # 省略 # 將結果輸出到 $output_file # 省略
功能測試方法:
util.zsh 裏的兩個函數功能過於簡單,無需測試。
測試 main.zsh 時,須要構造一系列測試用的 util.zsh,用於模擬各類狀況:
# 進程存在的狀況 check_process() { echo $$ } # 進程不存在的狀況 check_process() { return 1 } # 進程 process1 存在而 process2 不存在的狀況 check_process() { [[ $1 == process1 ]] && echo 1234 && return [[ $1 == process2 ]] && return 1 } # 輸出了進程號,但實際進程不存在的狀況 check_process() { echo 0 } # 其餘狀況 # 省略 # 路徑存在的狀況 get_process_cwd() { [[ $1 == process1 ]] && echo /path/to/cwd1 && return [[ $1 == process2 ]] && echo /path/to/cwd2 && return } # 路徑不存在的狀況 get_process_cwd() { return 1 } # 輸出了路徑,但路徑實際不存在的狀況 get_process_cwd() { echo /wrong/path } # 其餘狀況 # 省略
而後組合這些狀況,寫測試腳本判斷 main.zsh 的處理是否符合預期。
其中一個測試腳本樣例:
util_test1.zsh 內容:
#!/bin/zsh # 進程存在 check_process() { echo $$ } # 直接返回正確的路徑 get_process_cwd() { [[ $1 == process1 ]] && echo /path/to/cwd1 && return [[ $1 == process2 ]] && echo /path/to/cwd2 && return }
test.zsh 內容:
#!/bin/zsh # 用於測試的函數,能夠獨立成單獨腳本以便複用 assert_ok() { (($1 == 0)) || { echo Error, retcode: $1 exit 1 } } check_output_file() { # 檢查輸出文件是否符合預期 # 省略 } # 應用 util_test1.zsh ln -sf util_test1.zsh util.zsh # 運行腳本 ./main.zsh # 檢查返回值是否正常 assert_ok $? # 檢查輸出文件是否符合預期 check_output_file /path/to/output/file # 其餘檢查 # 省略 # 應用 util_test2.zsh ln -sf util_test2.zsh util.zsh # 省略
測試完每一個腳本的功能後,須要將各個腳本以及其餘程序整合起來測試互相調用過程是否正常。若是功能比較複雜,須要分批整合,測試各個邏輯單元是否能正常工做。在這部分測試中,和外部環境交互的腳本若是邏輯較爲簡單,能夠不參與,用模擬腳本替代。能夠手動測試或自動測試。一樣不能只測試正常狀況。
將全部相關組件整合起來,測試整個系統或者子系統的功能。模擬腳本不能參與系統測試,必須使用真實的外部環境。系統測試一般須要手動進行,能夠用自動化測試系統來輔助。須要覆蓋儘量多的狀況,不能只測試系統的正常功能。
本文簡單介紹了 shell 腳本的測試方法,以及編寫可測試代碼的方法。
本文再也不更新,全系列文章在此更新維護:github.com/goreliu/zshguide
付費解決 Windows、Linux、Shell、C、C++、AHK、Python、JavaScript、Lua 等領域相關問題,靈活訂價,歡迎諮詢,微信 ly50247。