Chinese (Traditional) (中文(繁體)) translation by Qiang Ji (you can also view the original English article)
在本教程中我將使用被Go語言設計者和社區總結出的最佳實踐教會你們在Go語言中所有慣用測試的基礎。 Go語言測試的主要武器是標準測試包。 測試對象是一個能解決Euler項目中一個簡單問題的示例程式。
平方和差
平放和差問題很簡單:“找出第一百個自然數的平方和與和的平方的差值。”
這個特別的問題可以被很簡單地解決,特別是如果你知道高斯。 比如,第N個自然數的和是(1 + N) * N / 2
,第N個整數的平方和是:(1 + N) * (N * 2 + 1) * N / 6
。 所以整個問題可以通過以下公式解決,將100分配給N:
(1 + N) * (N * 2 + 1) * N / 6 - ((1 + N) * N / 2) * ((1 + N) * N / 2)
是的,這非常的具體,沒有太多的東西可以測試。 相反地,我創建了一些比這個問題所需要的更多的功能,這可以在將來為其它程式提供服務(項目歐拉現在有559個問題)。
我把代碼放在了GitHub上。
以下是四個函數的簽名:
// The MakeIntList() function returns an array of consecutive integers // starting from 1 all the way to the `number` (including the number) func MakeIntList(number int) []int // The squareList() function takes a slice of integers and returns an // array of the quares of these integers func SquareList(numbers []int) []int // The sumList() function takes a slice of integers and returns their sum func SumList(numbers []int) int // Solve Project Euler #6 - Sum square difference func Process(number int) int
現在,通過我們的目標程式(請原諒我,TDD的狂熱份子),讓我們看看如何為這個程式編寫測試。
測試包
測試包是與go test
命令結合使用的。 你的包測試應該在一個以“_test.go”結尾的文件裏。 你可以將測試拆分為遵循此約定的多個文件。 例如:“whatever1_test.go”和“whatever2_test.go”。 你應該在這些測試文件中編寫你的測試函數。
每個測試函數是一個公開導出的名字以“Test”開始的函數,它接受一個指向testing.T
對象的指針,並且什麼都不返回。 它看上去像這樣:
func TestWhatever(t *testing.T) { // Your test code goes here }
T對象提供了各種可用於指示故障或紀錄錯誤的方法。
記住:只有定義在測試文件中的測試函數將被go test
命令執行。
編寫測試
每個測試遵循同樣的流程:設置測試環境(可選),提供測試輸入的代碼,捕獲測試結果,並且將其與預期輸出進行比較。 請注意,輸入和結果不一定是函數的參數。
如果被測數據是從數據庫中獲取數據,那麼輸入將確保數據庫包含適當的測試數據(可能涉及到各種級別的模擬)。 但是,對於我們的應用程式,常用情況是將輸入參數傳遞給函數並將結果與函數輸出進行比較,這就足夠了。
讓我們以SumList()
函數開始。 此函數以一組整數作為輸入並返回它們的和。 以下是一個可鑒別SumList()
函數應該有的行為的的測試函數。
它有兩個測試用例,如果預期輸出與結果不匹配,則調用test.T對象的Error()
方法。
func TestSumList_NotIdiomatic(t *testing.T) { // Test []{} -> 0 result := SumList([]int{}) if result != 0 { t.Error( "For input: ", []int{}, "expected:", 0, "got:", result) } // Test []{4, 8, 9} -> 21 result = SumList([]int{4, 8, 9}) if result != 21 { t.Error( "For input: ", []int{}, "expected:", 0, "got:", result) } }
這都是很直觀的,但它看上去有點囉嗦。 慣用的Go測試使用表驅動測試,你可以在其中定義一組輸入和預期輸出的結構,然後將這些一對對的列表在一個循環中提供給相同的邏輯。 以下是測試SumList()
函數的方法。
type List2IntTestPair struct { input []int output int } func TestSumList(t *testing.T) { var tests = []List2IntTestPair{ {[]int{}, 0}, {[]int{1}, 1}, {[]int{1, 2}, 3}, {[]int{12, 13, 25, 7}, 57}, } for _, pair := range tests { result := SumList(pair.input) if result != pair.output { t.Error( "For input: ", pair.input, "expected:", pair.output, "got:", result) } } }
這樣更好。 這很容易去加入更多的測試用例。 這很容易在一個地方擁有全面的測試用例,如果你決定更改測試邏輯,則無需更改多個實例。
以下是測試SquareList()
函數的另一個例子。 在這種情況下,輸入和輸出均為整數數組,因此測試組對結構是不同,但流程是相同的。 一個有趣的事情是,Go不提供內置的數組比對的方法,所以我使用reflect.DeepEqual()
將輸出數組與預期數組進行比較。
type List2ListTestPair struct { input []int output []int } func TestSquareList(t *testing.T) { var tests = []List2ListTestPair{ {[]int{}, []int{}}, {[]int{1}, []int{1}}, {[]int{2}, []int{4}}, {[]int{3, 5, 7}, []int{9, 25, 49}}, } for _, pair := range tests { result := SquareList(pair.input) if !reflect.DeepEqual(result, pair.output) { t.Error( "For input: ", pair.input, "expected:", pair.output, "got:", result) } } }
運行測試
運行測試與在你的包目錄中輸入go test
一樣簡單。 Go會找到帶有“_test.go”後綴的所有文件以及帶有“Test”前綴的所有函數,並將其作為測試運行。 以下是當一切測試正常時的樣子:
(G)/project-euler/6/go > go test PASS ok _/Users/gigi/Documents/dev/github/project-euler/6/go 0.006s
沒有很戲劇性。 讓我故意破壞一個測試。 我將更改SumList()
的測試用例,以使求1與2和的預期輸出為7。
func TestSumList(t *testing.T) { var tests = []List2IntTestPair{ {[]int{}, 0}, {[]int{1}, 1}, {[]int{1, 2}, 7}, {[]int{12, 13, 25, 7}, 57}, } for _, pair := range tests { result := SumList(pair.input) if result != pair.output { t.Error( "For input: ", pair.input, "expected:", pair.output, "got:", result) } } }
現在當你運行go test
,你能得到:
(G)/project-euler/6/go > go test --- FAIL: TestSumList (0.00s) 006_sum_square_difference_test.go:80: For input: [1 2] expected: 7 got: 3 FAIL exit status 1 FAIL _/Users/gigi/Documents/dev/github/project-euler/6/go 0.006s
它很好地陳述了發生了什麼,它也應該給了你解決問題所需要的所有信息。 在這種情況下,問題是測試自己本身是錯誤的,期望值應該是3。這是一個重要的教訓。 不要自動假設如果測試失敗,那測試中的代碼就是有問題的。 考慮整個系統,其中包括被測代碼,測試本身和測試環境。
測試覆蓋率
確保你的代碼能工作,僅僅通過測試是不夠的。 另一個重要方面是測試覆蓋率。 你的測試覆蓋了代碼中的每個語句? 有時即使你做到了這也是不夠的。 例如,如果你的代碼中有個循環一直運行直到一個條件被滿足,你可以成功地用一個條件去測試它,但沒有註意到在某些情況下該條件可能總是假的,這將導致無限循環。
單元測試
單元測試就像刷牙和使用牙線。 你不應該忽視它們。 它們是防禦問題的第一到防線,並讓你有信心重構代碼。 當嘗試重現問題並且能夠編寫一個失敗的測試來證明問題在修正後已經不存在,它們也是一種好事兒。
集成測試
集成測試也是必要的。 把它們想像成去看牙醫。 有段時間不需要它們你也許也OK,但是如果你忽略它們太長時間,那就不好了。
大多數複雜的程序是由多個相互關聯的模塊或組件組成的。 問題常常會在一起編寫這些組件時發生。 集成測試可以讓你確信整個系統按預期運行。 還有許多其它類型的測試,如驗收測試,性能測試,壓力/負載測試和全面的整個系統測試,但單元測試和集成測試是測試軟件的兩個基本方法。
結論
Go具有內置的測試支持,一種優良的測試方式,以及以表驅動測試形式被推薦的指導方針。
對每個輸入和輸出組合編寫特殊struct的需求是有點煩人,但這是你對Go語言簡單的設計方法支付的代價。
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Update me weeklyEnvato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post