測試用例是開發人員最後一塊遮羞布

測試用例是開發人員最後一塊遮羞布

最近一週寫一個比較複雜的業務模塊,越寫到後面真心越心虛。操做愈來愈複雜了,代碼也逐漸凌亂了起來。好比一個接口,傳入的是一個比較複雜的大json,我須要解析這個大json,而後根據json中字段進行增刪改查,調用第三方服務等操做。告訴前端接口已經完成的時候,老是有點沒有底氣。說實話,在寫PHP的時候,我確實不多寫單元測試,大都是對着頁面進行一波一波的測試,如今想一想,一個是懶,還有一個是確實PHP是不須要編譯的語言,沒有編譯時間,測試-修正,整個流程很是短。可是此次是一個比較大的GoLang項目,若是仍是按照「編譯-起服務-調用-調整代碼-編譯-起服務-調用-...」 這種循環來作調試,真是會瘋了的。因此我能靜下心好好研究研究如何寫Golang的單元測試了。前端

數據庫怎麼辦?

這個是第一個須要思考的問題。這個問題和語言無關。一旦有數據庫操做,就須要考慮如何在測試用例中如何處理數據庫操做。我想了想,無外乎兩種作法,一種是直接mock數據庫的返回對象。另一種,是搭建一個測試DB,而後灌入假數據,進行測試。這兩種方式我選擇了後一種。有幾個理由:首先,mock數據庫返回數據是一個比灌入DB數據更爲複雜的邏輯,數據庫返回的數據根據sql各類各樣,要想在每一個環節都寫好數據庫操做返回,倒不如我直接僞造一些數據來的方便。其次,mock數據庫返回會丟失model層的測試邏輯,固然若是你是輕model層,整個model就只有一個orm,這個可能就不是理由了。mysql

因此,我操做的第一步,從線上把數據庫表結構copy一份到我本地vagrant的mysql中。git

這裏必需要注意,你的測試數據庫和測試代碼最好是同一個機器上,不然每跑一個測試用例,消耗的時間很是大,你的測試體驗也不會太好。github

第三方請求怎麼辦?

個人代碼邏輯中也有一些第三方調用,調用其餘服務。固然這裏也有一樣的兩種辦法,一種是直接在本地測試環境搭建第三方服務,另一種是mock第三方服務的返回數據。這裏我選擇了mock數據的方式。基本想法是由於我這個測試畢竟不是一種全鏈路測試,測試的主體仍是個人服務,個人服務基本上只包含服務+DB,若是要搭建第三方服務,這就有點捨本逐末的感受了。golang

好了,如何mock第三方服務呢?web

查了下golang中mock的包有兩個比較出名,一個是golang官網出品的golang/mock,另一個是monkey(https://github.com/bouk/monkey)。兩個相比之下,我感受golang/mock是師出有名,可是不如monkey好用,monkey屬於黑科技,使用修改函數指針的方式進行mock函數。我想了想,實用第一位,投入了monkey的懷抱。sql

基本使用代碼以下:數據庫

// mock路網接口
guard := monkey.Patch(lib.Curl, func(trace *lib.TraceContext, CurlType, urlString string, data url.Values, addToken bool) ([]byte, error) {
  return []byte("{[\"10010\":\"後廠村路\"}"]), nil
})
defer guard.Unpatch()

將lib.Curl整個函數給mock了,而且在函數結束後修改mock的函數,保證不影響其餘測試用例。json

配置文件怎麼辦?

web服務通常都會有讀取配置的代碼,個人服務是讀取一個參數config=base.json來進行配置的讀取的。go test中是沒有辦法給test的代碼傳遞參數的,(我看網上的一些文章說有個-args的參數,可是我在go1.11版本中確實沒有看到這個參數)。因而我只能選擇使用環境變量的方式。在運行go test的時候,在最開頭的部分設置下當前這個go test的環境變量CONFIG_PATH,而後修改下個人初始化配置文件的代碼,容許傳入參數進行配置文件的讀取。架構

大概代碼以下:

在運行go test的時候設置環境變量:

CONFIG_PATH=/home/vagrant/foo/conf/yejianfeng/base.json go test foo/signaledit/... -v -test.run TestGetGroups

測試環境的初始化配置文件邏輯:

package test

import (
	..
)

var HasSetup = false

// signalEdit初始化,只調用一次
func SetUpSignalEdit() {
	if HasSetup == false {
		gin.SetMode(gin.TestMode)

		confPath := os.Getenv("CONFIG_PATH") // 獲取環境變量
		commonlib.Init(confPath, "")    // 初始化配置文件
		conf.ParseLocalConfig()
		db.InitDB()

		HasSetup = true
	}

	DestroyTestData(db.EditDB)
	CreateTestData(db.EditDB)
}

web怎麼進行單元測試?

關於這個,httptest這個包提供給咱們想要的邏輯了,網上的文章也一大堆了。使用起來也是很方便,

router := gin.New()
jc := Controller{}
// 燈組模型表獲取信息
router.GET("/group/all", jc.GroupAll)
...
//構建返回值
w := httptest.NewRecorder()
//構建請求
r, _ := http.NewRequest("GET", "/group/all?logic_junction_id=test_junction", nil)
//調用請求接口
router.ServeHTTP(w, r)

resp := w.Result()
body, _ := ioutil.ReadAll(resp.Body)

就沒有什麼好說的了。

關於數據初始化和銷燬

既然我選擇使用本地DB進行測試,那麼按照邏輯,須要在測試用例開始初始化DB數據,而後在測試用例結束後銷燬數據。這裏我還選擇在測試用例開始的時候,先銷燬數據,而後初始化數據,測試用例結束的時候不要銷燬數據。這樣作我認可有很差的地方,就是有可能會有髒數據。比較好的地方,就是我在單個測試用例跑完的時候,我有機會去數據庫看一眼如今數據庫裏面的測試數據是什麼樣子。

無論怎麼洋,數據初始化和銷燬的工做就變得異常重要了,它們必須是冪等,並且能夠循環冪等。(銷燬-初始化)=(銷燬-銷燬-初始化)=(初始化-銷燬-初始化)。要作到這個個人感覺必須藉助具體的業務數據表邏輯了。好比個人全部數據表都有一個路口id的字段,那麼我就很容易作到銷燬的冪等,我每次銷燬的時候,就只要把這個路口的全部數據刪除就能夠了。若是沒有的話,因爲咱們的數據庫是本地數據庫,不妨採用整個數據表清空的方式操做。

數據初始化和銷燬的函數我封裝成兩個函數,放在一個包裏面

var (
	SignalID        = int64(999999)
	LogicJunctionId = "test_junction"
)

// 建立測試數據
func CreateTestData(db *gorm.DB) {

	// SignalInfo表建立一條數據
	signalInfo := &models.SignalInfo{}
	signalInfo.Id = SignalID
	signalInfo.Name = "測試路口id"
	signalInfo.LogicJunctionId = LogicJunctionId
	signalInfo.Status = 1
	db.Create(signalInfo)
}
// 銷燬測試數據
func DestroyTestData(db *gorm.DB) {

	db.Delete(&models.SignalInfo{}, "logic_junctionid=" + LogicJunctionId)

  ...
}

而後把上面說的初始化操做封裝成一個函數

var HasSetup = false

// signalEdit初始化,只調用一次
func SetUpSignalEdit() {
	if HasSetup == false {
		gin.SetMode(gin.TestMode)
		confPath := os.Getenv("CONFIG_PATH")
		commonlib.Init(confPath, "")
		conf.ParseLocalConfig()
		db.InitDB()

		HasSetup = true
	}
	DestroyTestData(db.EditDB)
	CreateTestData(db.EditDB)
}

全部測試用例都先調用下這個函數

func TestGetGroups(t *testing.T) {
	test.SetUpSignalEdit()
  ...
}

這裏真心要吐槽下testing框架,既然作了測試框架,SetUp函數,SetDown函數這些都不考慮,和主流的測試框架的思想真的有點誤差,致使像這種「普通」的初始化的需求都要本身寫方法來繞過,至少testing框架爲應用思考的東西仍是太少了。

測試用例的粒度

我一直知道寫好測試用例是一個難度不亞於開發的工做。測試用例有粒度問題,我以爲,測試用例的粒度宜大不宜小。我這個項目是controller-service-mmodels架構,controller一個函數就是一個接口,service一個函數是一個通用性比較高的服務,model是比較瘦的model,基本只作增刪改查。在我這個架構中,我寫的測試用例粒度大多數是controller級別的,有少數是service級別的,model級別的測試用例基本沒有。

測試用例粒度大一些,有個明顯的好處,就是對需求的容忍度高了不少。通常測試用例最痛的就是需求一旦修改了,個人業務邏輯就修改了,個人測試用例也要跟着修改。修改測試用例是很痛苦的事情。因此若是測試用例足夠大,好比和接口同樣大,那麼基本上,因爲業務接口的兼容性要求,咱們的測試用例的輸入輸出通常不會進行大的變更(雖然裏面的service或者model會進行比較大的變更)。這樣有一些需求變化了以後,我甚至不須要修改任何測試用例的代碼就能夠。

固然有的測試用例粒度太大,一些小的分支可能就測試不到,或者很難構建測試數據,因此有的時候,仍是須要一寫稍微小一點的粒度的測試用例。

另外對於不須要依賴測試數據的類庫函數,若是你對這個類庫函數的輸入輸出的需求變動有把握控制的話,(你須要對本身的這個判斷負責)這種類庫函數的測試用例則是越細越好。

其餘原則性的東西

說說寫測試用例的一些原則性的東西。

檢驗邏輯抗需求變動能力越強越好

首先,測試用例的檢驗邏輯不是越全越好,並且有不少技巧。好比一個插入的接口,你測試是否插入成功,有不少時候,你根據判斷插入條數是否多一條會比你判斷這個插入條數的全部字段是不是你要求的更好。原則仍是那個,測試用例的抗需求變動能力會更高,首先基本上若是個人插入邏輯很簡單,那麼插入成功就約等於插入的每一個字段都知足,固然這裏是約等於,可是由於業務代碼也是我本身寫的,內心這個B數仍是有的。而後,若是一旦需求變動我這個數據多了一個字段,那麼我這個測試用例基本不須要作任何修改就還能夠繼續跑起來。

再次強調下,這裏的約等於的判斷就是看你對你業務代碼的感受了。

並非全部的錯誤都須要完美處理

測試代碼畢竟不像業務代碼那麼須要完美的嚴謹,全部的panic都是歡迎的。換句話說,咱們業務代碼基本上對全部error都須要有所處理,可是測試用例並不必定了。若是我在上一行代碼中沒有處理這個error,那麼我傳遞給下一行的參數極可能就是nil,頗有可能在下一行代碼中直接panic了一個錯誤出來。這個也能讓我發現個人錯誤。

因此,測試用例並不須要寫那麼嚴謹,有的地方直接panic錯誤也是一個很好的選擇。

Fatal和Error的選擇

基本上我以爲Error沒啥用,我目前的測試用例都要求全部的判斷節點都跑成功,任何一個地方失敗了,直接就報錯進行調試。個人精力也不容許我一次性能處理多個錯誤case,基本上調試失敗的測試用例是一個個調試的,因此error並無什麼用。

這點純粹我我的觀點,估計會有不少人不一樣意。

檢驗邏輯多用變量

檢驗邏輯儘可能少用 response.Name == "測試路口" 這種代碼,能儘可能找到替換"測試路口" 這個的變量儘可能使用變量,一樣的理由,測試用例的抗需求變動能力會更高。

總結

測試用例是開發人員最後一塊遮羞布,寫Golang的代碼和寫PHP的代碼確實體驗徹底不同,在Golang代碼中,首先寫測試用例異常方便了。其次,Golang的調試成本遠遠高於PHP,寫測試用例看起來是浪費時間,其實是節省你的調試時間。最後,golang代碼的每次重構(增長一個字段,少一個字段)影響的文件數遠遠高於PHP,若是沒有這塊遮羞布,你怎麼確保你的代碼修改後還能正常運行呢?

Just Testing!

相關文章
相關標籤/搜索