一個測試工程師走進一家酒吧……

{"type":"doc","content":[{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個測試工程師走進一家酒吧,要了一杯啤酒;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個測試工程師走進一家酒吧,要了一杯咖啡;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個測試工程師走進一家酒吧,要了 0.7 杯啤酒;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個測試工程師走進一家酒吧,要了-1 杯啤酒;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個測試工程師走進一家酒吧,要了 2^32 杯啤酒;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個測試工程師走進一家酒吧,要了一杯洗腳水;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個測試工程師走進一家酒吧,要了一杯蜥蜴;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個測試工程師走進一家酒吧,要了一份 asdfQwer@24dg!&*(@;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個測試工程師走進一家酒吧,什麼也沒要;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個測試工程師走進一家酒吧,又走出去又從窗戶進來又從後門出去從下水道鑽進來;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個測試工程師走進一家酒吧,又走出去又進來又出去又進來又出去,最後在外面把老闆打了一頓;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個測試工程師走進一家酒吧,要了一杯燙燙燙的錕斤拷;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個測試工程師走進一家酒吧,要了 NaN 杯 Null;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個測試工程師衝進一家酒吧,要了 500T 啤酒咖啡洗腳水野貓狼牙棒奶茶;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個測試工程師把酒吧拆了;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個測試工程師化裝成老闆走進一家酒吧,要了 500 杯啤酒並且不付錢;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一萬個測試工程師在酒吧門外呼嘯而過;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個測試工程師走進一家酒吧,要了一杯啤酒';DROP TABLE 酒吧;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"測試工程師們滿意地離開了酒吧。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後一名顧客點了一份炒飯,酒吧炸了。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面是網上流行的一個關於測試的笑話,其主要核心思想是——你永遠無法把所有問題都充分測試。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在軟件工程中,測試是極其重要的一環,比重通常可以與編碼相同,甚至大大超過。那麼在 Golang 裏,怎麼樣把測試寫好,寫正確?本文將對這個問題做一些簡單的介紹。 當前文章將主要分兩個部分:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Golang 測試的一些基本寫法和工具"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如何寫“正確”的測試,這個部分雖然代碼是用 golang 編寫,但是其核心思想不限語言"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於篇幅問題,本文將不涉及性能測試,之後會另起一篇來談。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"爲什麼要寫測試"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們舉個不太恰當的例子,測試也是代碼,我們假定寫代碼時出現 bug 的概率是 p(0

0,t(i+1)爲對於 t(i)的測試,t(i+1)正確爲 t(i)正確的必要條件,那麼對所有的 i,i>0,t(i)正確都是 t(0)正確的必要條件。。。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/59\/59926aa33715b243e0541d2e2b60c02d.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"測試的種類"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"測試的種類有非常多,我們這裏只挑幾個對一般開發者來說比較重要的測試,做簡略的說明。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"白盒測試、黑盒測試"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先是從測試方法上可以分爲白盒測試和黑盒測試(當然還存在所謂的灰盒測試,這裏不討論)"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"白盒測試 (White-box testing):白盒測試又稱透明盒測試、結構測試等,軟件測試的主要方法之一,也稱結構測試、邏輯驅動測試或基於程序本身的測試。測試應用程序的內部結構或運作,而不是測試應用程序的功能。在白盒測試時,以編程語言的角度來設計測試案例。測試者輸入數據驗證數據流在程序中的流動路徑,並確定適當的輸出,類似測試電路中的節點。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"黑盒測試 (Black-box testing):黑盒測試,軟件測試的主要方法之一,也可以稱爲功能測試、數據驅動測試或基於規格說明的測試。測試者不瞭解程序的內部情況,不需具備應用程序的代碼、內部結構和編程語言的專門知識。只知道程序的輸入、輸出和系統的功能,這是從用戶的角度針對軟件界面、功能及外部結構進行測試,而不考慮程序內部邏輯結構。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們寫的單元測試一般屬於白盒測試,因爲我們對測試對象的內部邏輯有着充分了解。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"單元測試、集成測試"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從測試的維度上,又可以分爲單元測試和集成測試:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在計算機編程中,單元測試又稱爲模塊測試,是針對程序模塊來進行正確性檢驗的測試工作。程序單元是應用的最小可測試部件。在過程化編程中,一個單元就是單個程序、函數、過程等;對於面向對象編程,最小單元就是方法,包括基類、抽象類、或者派生類中的方法。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"整合測試又稱組裝測試,即對程序模塊採用一次性或增值方式組裝起來,對系統的接口進行正確性檢驗的測試工作。整合測試一般在單元測試之後、系統測試之前進行。實踐表明,有時模塊雖然可以單獨工作,但是並不能保證組裝起來也可以同時工作。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"單元測試可以是黑盒測試,集成測試亦可以是白盒測試"}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"迴歸測試"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"迴歸測試是軟件測試的一種,旨在檢驗軟件原有功能在修改後是否保持完整。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"迴歸測試主要是希望維持軟件的不變性,我們舉一個例子來說明。例如我們發現軟件在運行的過程中出現了問題,在 gitlab 上開啓了一個 issue。之後我們並且定位到了問題,我們可以先寫一個測試(測試的名稱可以帶上 issue 的 ID)來複現問題(該版本代碼運行此測試結果失敗)。之後我們修復問題後,再次運行測試,測試的結果應當成功。那麼我們之後每次運行測試的時候,通過運行這個測試,可以保證同樣的問題不會復現。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"一個基本的測試"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們先來看一個 Golang 的代碼:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"\/\/ add.go\npackage add\n\nfunc Add(a, b int) int {\n   return a + b\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個測試用例可以寫成:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"\/\/ add_test.go\npackage add\n\nimport (\n   \"testing\"\n)\n\nfunc TestAdd(t *testing.T) {\n   res := Add(1, 2)\n   if res != 3 {\n      t.Errorf(\"the result is %d instead of 3\", res)\n   }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在命令行我們使用 go test"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"go test\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個時候 go 會執行該目錄下所有的以_test.go 爲後綴中的測試,測試成功的話會有如下輸出:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"% go test\nPASS\nok      code.byted.org\/ek\/demo_test\/t01_basic\/correct       0.015s\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設這個時候我們把 Add 函數修改成錯誤的實現"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" \/\/ add.go\npackage add\n\nfunc Add(a, b int) int {\n   return a - b\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"再次執行測試命令"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"% go test\n--- FAIL: TestAddWrong (0.00s)\n    add_test.go:11: the result is -1 instead of 3\nFAIL\nexit status 1\nFAIL    code.byted.org\/ek\/demo_test\/t01_basic\/wrong 0.006s\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"會發現測試失敗。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"只執行一個測試文件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼如果我們想只測試這一個文件,輸入"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"go test add_test.go\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"會發現命令行輸出"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"% go test add_test.go\n# command-line-arguments [command-line-arguments.test]\n.\/add_test.go:9:9: undefined: Add\nFAIL    command-line-arguments [build failed]\nFAIL\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這是因爲我們沒有附帶測試對象的代碼,修改測試後可以獲得正確的輸出:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"% go test add_test.go add.go\nok      command-line-arguments  0.007s\n"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"測試的幾種書寫方式"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"子測試"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常來說我們測試某個函數和方法,可能需要測試很多不同的 case 或者邊際條件,例如我們爲上面的 Add 函數寫兩個測試,可以寫成:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" \/\/ add_test.go\npackage add\n\nimport (\n   \"testing\"\n)\n\nfunc TestAdd(t *testing.T) {\n   res := Add(1, 0)\n   if res != 1 {\n      t.Errorf(\"the result is %d instead of 1\", res)\n   }\n}\n\nfunc TestAdd2(t *testing.T) {\n   res := Add(0, 1)\n   if res != 1 {\n      t.Errorf(\"the result is %d instead of 1\", res)\n   }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"測試的結果:(使用-v 可以獲得更多輸出)"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"% go test -v\n=== RUN   TestAdd\n--- PASS: TestAdd (0.00s)\n=== RUN   TestAdd2\n--- PASS: TestAdd2 (0.00s)\nPASS\nok      code.byted.org\/ek\/demo_test\/t02_subtest\/non_subtest     0.007s\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另一種寫法是寫成子測試的形式"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"\/\/ add_test.go\npackage add\n\nimport (\n   \"testing\"\n)\n\nfunc TestAdd(t *testing.T) {\n   t.Run(\"test1\", func(t *testing.T) {\n      res := Add(1, 0)\n      if res != 1 {\n         t.Errorf(\"the result is %d instead of 1\", res)\n      }\n   })\n   t.Run(\"\", func(t *testing.T) {\n      res := Add(0, 1)\n      if res != 1 {\n         t.Errorf(\"the result is %d instead of 1\", res)\n      }\n   })\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"執行結果:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"% go test -v\n=== RUN   TestAdd\n=== RUN   TestAdd\/test1\n=== RUN   TestAdd\/#00\n--- PASS: TestAdd (0.00s)\n    --- PASS: TestAdd\/test1 (0.00s)\n    --- PASS: TestAdd\/#00 (0.00s)\nPASS\nok      code.byted.org\/ek\/demo_test\/t02_subtest\/subtest 0.007s\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到輸出中會將測試按照嵌套的結構分類,子測試的嵌套沒有層數限制,如果不寫測試名的話,會自動按照順序給予序號作爲其測試名(例如上面的#00)"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"對 IDE(Goland)友好的子測試"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有一種測試的寫法是:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"tcList := map[string][]int{\n   \"t1\": {1, 2, 3},\n   \"t2\": {4, 5, 9},\n}\nfor name, tc := range tcList {\n   t.Run(name, func(t *testing.T) {\n      require.Equal(t, tc[2], Add(tc[0], tc[1]))\n   })\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看上去沒什麼問題,然而有一個缺點是,這個測試對 IDE 並不友好:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/79\/79c622c03b202088a1f8ebaba9a15706.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們無法在出錯的時候對單個測試重新執行 所以推薦儘可能對每個 t.Run 都要獨立書寫,例如:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"f := func(a, b, exp int) func(t *testing.T) {\n   return func(t *testing.T) {\n      require.Equal(t, exp, Add(a, b))\n   }\n}\nt.Run(\"t1\", f(1, 2, 3))\nt.Run(\"t2\", f(4, 5, 9))"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/56\/569cad132db865846b91302381f547e5.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"測試分包"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們上面的 add.go 和 add_test.go 文件都處於同一個目錄下,頂部的 package 名稱都是 add,那麼在寫測試的過程中,也可以爲測試啓用與非測試文件不同的包名,例如我們現在將測試文件的包名改爲 add_test:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" \/\/ add_test.go\npackage add_test\n\nimport (\n   \"testing\"\n)\n\nfunc TestAdd(t *testing.T) {\n   res := Add(1, 2)\n   if res != 3 {\n      t.Errorf(\"the result is %d instead of 3\", res)\n   }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個時候執行 go test 會發現"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"% go test\n# code.byted.org\/ek\/demo_test\/t03_diffpkg_test [code.byted.org\/ek\/demo_test\/t03_diffpkg.test]\n.\/add_test.go:9:9: undefined: Add\nFAIL    code.byted.org\/ek\/demo_test\/t03_diffpkg [build failed]\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於包名變化了,我們無法再訪問到 Add 函數,這個時候我們增加 import 即可:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" \/\/ add_test.go\npackage add_test\n\nimport (\n   \"testing\"\n\n   . \"code.byted.org\/ek\/demo_test\/t03_diffpkg\"\n)\n\nfunc TestAdd(t *testing.T) {\n   res := Add(1, 2)\n   if res != 3 {\n      t.Errorf(\"the result is %d instead of 3\", res)\n   }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們使用上面的方式來導入包內的函數即可。 但使用了這種方式後,將無法訪問包內未導出的函數(以小寫開頭的)。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"測試的工具庫"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"github.com\/stretchr\/testify"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以使用強大的 testify 來方便我們寫測試 例如上面的測試我們可以用這個庫寫成:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" \/\/ add_test.go\npackage correct\n\nimport (\n   \"testing\"\n\n   \"github.com\/stretchr\/testify\/require\"\n)\n\nfunc TestAdd(t *testing.T) {\n   res := Add(1, 2)\n   require.Equal(t, 3, res)\n\n   \/*\n    must := require.New(t)\n    res := Add(1, 2)\n    must.Equal(3, res)\n    *\/\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果執行失敗,則會在命令行看到如下輸出:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"% go test\nok      code.byted.org\/ek\/demo_test\/t04_libraries\/testify\/correct       0.008s\n--- FAIL: TestAdd (0.00s)\n    add_test.go:12:\n                Error Trace:    add_test.go:12\n                Error:          Not equal:\n                                expected: 3\n                                actual  : -1\n                Test:           TestAdd\nFAIL\nFAIL    code.byted.org\/ek\/demo_test\/t04_libraries\/testify\/wrong 0.009s\nFAIL\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"庫提供了格式化的錯誤詳情(堆棧、錯誤值、期望值等)來方便我們調試。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"github.com\/DATA-DOG\/go-sqlmock"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於需要測試 sql 的地方可以使用 go-sqlmock 來測試"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"優點:不需要依賴數據庫"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"缺點:脫離了數據庫的具體實現,所以需要寫比較複雜的測試代碼"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"github.com\/golang\/mock"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"強大的對 interface 的 mock 庫,例如我們要測試函數 ioutil.ReadAll"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"func ReadAll(r io.Reader) ([]byte, error)\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們 mock 一個 io.Reader"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"\/\/ package: 輸出包名\n\/\/ destination: 輸出文件\n\/\/ io: mock對象的包\n\/\/ Reader: mock對象的interface名\nmockgen -package gomock -destination mock_test.go io Reader\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以在目錄下看到 mock_test.go 文件裏,包含了一個 io.Reader 的 mock 實現 我們可以使用這個實現去測試 ioutil.Reader,例如"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"ctrl := gomock.NewController(t)\ndefer ctrl.Finish()\nm := NewMockReader(ctrl)\nm.EXPECT().Read(gomock.Any()).Return(0, errors.New(\"error\"))\n_, err := ioutil.ReadAll(m)\nrequire.Error(t, err)\n"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"net\/http\/httptest"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常我們測試服務端代碼的時候,會先啓動服務,再啓動測試。官方的 httptest 包給我們提供了一種方便地啓動一個服務實例來測試的方法。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"其他"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其他一些測試工具可以前往 awesome-go#testing 查找"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/github.com\/avelino\/awesome-go#testing"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"如何寫好測試"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面介紹了測試的基本工具和寫法,我們已經完成了“必先利其器”,下面我們將介紹如何“善其事”。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"併發測試"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在平時,大家寫服務的時候,基本都必須考慮併發,我們使用 IDE 測試的時候,IDE 默認情況下並不會主動測試併發狀態,那麼如何保證我們寫出來的代碼是併發安全的? 我們來舉個例子,比如我們有個計數器,作用就是計數。"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"type Counter int32\n\nfunc (c *Counter) Incr() {\n   *c++\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"很顯然這個計數器在併發情況下是不安全的,那麼我們如何寫一個測試來做這個計數器的併發測試呢?"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"import (\n   \"sync\"\n   \"testing\"\n\n   \"github.com\/stretchr\/testify\/require\"\n)\n\nfunc TestA_Incr(t *testing.T) {\n   var a Counter\n   eg := sync.WaitGroup{}\n   count := 10\n   eg.Add(count)\n   for i := 0; i  0 && j > 0 {\n      i += j\n   }\n   return i\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面的測試用例覆蓋率達到了 100%,但是並沒有測試到所有的分支"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"func TestAdd(t *testing.T) {\n   res := AddIfBothPositive(1, 2)\n   require.Equal(t, 3, res)\n}\n"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"並沒有處理異常\/邊界條件"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"func Divide(i, j int) int {\n   return i \/ j\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Divide 函數並沒有處理除數爲 0 的情況,而單元測試的覆蓋率是 100%"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"func TestAdd(t *testing.T) {\n   res := Divide(6, 2)\n   require.Equal(t, 3, res)\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的例子說明 100%的測試覆蓋並不是真的“100%覆蓋”了所有的代碼運行情況。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"覆蓋率的統計方法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"測試覆蓋率的統計方法一般是: 測試中執行到的代碼行數 \/ 測試的代碼的總行數 然而代碼在實際運行中,每一行運行到的概率、出錯的嚴重程度等等也是不同的,所以我們在追求高覆蓋率的同時,不能迷信覆蓋率。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"測試是不怕重複書寫的"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏的重複書寫,可以一定程度上認爲是“代碼複用”的反義詞。我們主要從下面的幾方面來說。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"重複書寫類似的測試用例"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"測試用例只要不是完全一致,那麼即便是比較雷同的測試用例,我們都可以認爲是有意義的,沒有必要爲了代碼的精簡特地刪除,例如我們測試上面的 Add 函數"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"func TestAdd(t *testing.T) {\n   t.Run(\"fixed\", func(t *testing.T) {\n      res := Add(1, 2)\n      require.Equal(t, 3, res)\n   })\n   t.Run(\"random\", func(t *testing.T) {\n      a := rand.Int()\n      b := rand.Int()\n      res := Add(a, b)\n      require.Equal(t, a+b, res)\n   })\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然第二個測試看起來覆蓋了第一個測試,但沒有必要去特地刪除第一個測試,越多的測試越能增加我們代碼的可靠性。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"重複書寫(源)代碼中的定義和邏輯"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"比如我們有一份代碼"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"package add\n\nconst Value = 3\n\nfunc AddInternalValue(a int) int {\n   return a + Value\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"測試爲"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"func TestAdd(t *testing.T) {\n   res := AddInternalValue(1)\n   require.Equal(t, 1+Value, res)\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看起來非常完美,但是如果某天內部變量 Value 的值被不小心改動了,那麼這個測試無法反應出這個改動,也就無法及時發現這個錯誤了。如果我們寫成"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"func TestAdd(t *testing.T) {\n   const value = 3\n   res := AddInternalValue(1)\n   require.Equal(t, 1+value, res)\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"就不用擔心無法發現常量值的變化了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文轉載自:字節跳動技術團隊(ID:toutiaotechblog)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:"},{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/9x8wdhDDMn-UBkTPG4SsCg","title":"xxx","type":null},"content":[{"type":"text","text":"一個測試工程師走進一家酒吧……"}]}]}]}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章