2300+ commits,80+ Contributors,阿里 PouchContainer 工程质量实践
距離阿里百萬級規(guī)模開源容器技術(shù) PouchContainer 宣布開源不到一年,2018 年 8 月 31 日,PouchContainer ?GA 版本發(fā)布,已經(jīng)完全達到生產(chǎn)級別。
?
PouchContainer 能夠在如此短的時間內(nèi)發(fā)布 GA 版本,離不開容器社區(qū)的支持,在超過 2300 個 commit 的背后,有 80 多位社區(qū)開發(fā)者的踴躍貢獻。由于每位貢獻者編碼習(xí)慣都不盡相同,代碼審閱者的責任不僅僅是關(guān)注邏輯正確性和性能問題,還應(yīng)該關(guān)注代碼風格,因為統(tǒng)一的代碼規(guī)范是保證項目代碼可維護的前提。除了統(tǒng)一項目代碼風格之外,測試用例的覆蓋率和穩(wěn)定性也是項目關(guān)注的重點。簡單設(shè)想下,在缺少回歸測試用例的項目,如何保證每次代碼更新都不會影響到現(xiàn)有功能?
?
本文會分享 PouchContainer 在代碼風格規(guī)范和 golang 單元測試用例方面的實踐。
?
本文作者傅偉(花名:聿歌),阿里巴巴高級開發(fā)工程師。負責阿里巴巴開源容器技術(shù) PouchContainer 研發(fā)和技術(shù)推廣。
1. 統(tǒng)一的編碼風格規(guī)范
PouchContainer 是由 golang 語言構(gòu)建的項目,項目里會使用 shell script 來完成一些自動化操作,比如編譯和打包操作。除了 golang 和 shell script 以外,PouchContainer 還包含了大量 Markdown 風格的文檔,它是使用者認識和了解 PouchContainer 的入口,它的規(guī)范排版和正確拼寫也是項目的關(guān)注對象。接下來的內(nèi)容將會介紹 PouchContainer 在編碼風格規(guī)范上使用的工具和使用場景。
1.1 Golinter - 統(tǒng)一代碼格式
golang 的語法設(shè)計簡單,加上社區(qū)一開始都有完備的 CodeReview 指導(dǎo),讓絕大部分的 golang 項目都有相同的代碼風格,很少陷入到無謂的?宗教?之爭。在社區(qū)的基礎(chǔ)上,PouchContainer 還定義了一些特定的規(guī)則來約定開發(fā)者,目的是為了保證代碼的可讀性,具體內(nèi)容可閱讀這里:
https://github.com/alibaba/pouch/blob/master/docs/contributions/code_styles.md#additional-style-rules
但光靠書面協(xié)議去做規(guī)范,這是很難保證項目代碼風格保持一致。因此 golang 和其他語言一樣,其官方提供了基礎(chǔ)的工具鏈,比如 golint, ?gofmt,goimports 以及 go vet 等等,這些工具可在編譯前檢查和統(tǒng)一代碼風格,為代碼審閱等后續(xù)流程提供了自動化的可能。目前 PouchContainer 在 每一次開發(fā)者提的 Pull Request 都會在 CircleCI 運行上述的代碼檢查工具。如果檢查工具顯示異常,代碼審閱者有權(quán) 拒絕 審閱,甚至可以拒絕合并代碼。
除了官方提供的工具外,我們還可以在開源社區(qū)中選擇第三方的代碼檢查工具,比如 errcheck 檢查開發(fā)者是否都處理了函數(shù)返回的 error 。但是這些工具并沒有統(tǒng)一的輸出格式,這很難完成不同工具輸出結(jié)果的整合。好在開源社區(qū)有人實現(xiàn)了這一層統(tǒng)一的接口,即 gometalinter,它可以整合各種代碼檢查工具,推薦采用的組合是:
-
golint - Google's (mostly stylistic) linter.
-
gofmt -s - Checks if the code is properly formatted and could not be further simplified.
-
goimports - Checks missing or unreferenced package imports.
-
go vet - Reports potential errors that otherwise compile.
-
varcheck - Find unused global variables and constants.
-
structcheck - Find unused struct fields
-
errcheck - Check that error return values are used.
-
misspell - Finds commonly misspelled English words.
每個項目都可以根據(jù)自己的需求來訂制 gometalinter 套餐。
1.2 Shellcheck - 減少 shell script 潛在問題
shell script 雖然功能強大,但是它依然需要語法檢查來避免一些潛在的、不可預(yù)判的錯誤。比如定義了未使用的變量,雖然不影響腳本的使用,但是它的存在會成為項目維護者的負擔。
#!/usr/bin/env?bashpouch_version=0.5.xdosomething()?{echo?"do?something" }dosomethingPouchContainer 會使用 shellcheck 來檢查目前項目里的 shell script。就以上述代碼為例,shellcheck 檢測會獲得未使用變量的警告。該工具可以在代碼審閱階段發(fā)現(xiàn) shell script 潛在的問題,減少運行時出錯的概率。
In?test.sh?line?3: pouch_version=0.5.x ^--?SC2034:?pouch_version?appears?unused.?Verify?it?or?export?it.PouchContainer 當前的持續(xù)集成任務(wù)會掃描項目里?.sh?腳本,并逐一使用 shellcheck 來檢查。
NOTE: 當 shellcheck 檢查太過于嚴格了,項目里可以通過加注釋的方式來避開檢查,或者是項目里統(tǒng)一關(guān)閉某項檢查。具體的檢查規(guī)則可查看這里:https://github.com/koalaman/shellcheck/wiki
1.3 Markdownlint - 統(tǒng)一文檔格式編排
PouchContainer 作為開源項目,它的文檔同代碼一樣重要,因為文檔是讓用戶了解 PouchContainer 的最佳方式。文檔采用 markdown 的方式來編寫,它的編排格式和拼寫錯誤都是項目重點照顧對象。
同代碼一樣,光有文本約定還是會出現(xiàn)漏判,所以 PouchContainer 采用 markdownlint 和 misspell 來檢查文檔格式和拼寫錯誤,這些檢查的地位同?golint?一樣,會在每次 Pull Request 都會在 CircleCI 中運行,一旦出現(xiàn)異常,代碼審閱者有權(quán)?拒絕?審閱或者合并代碼。
PouchContainer 當前的持續(xù)集成任務(wù)會檢查項目里的 markdown 文檔編排格式,同時還檢查了所有文件里的拼寫。
NOTE: 當 markdownlint 要求太過于嚴格時,項目里可以關(guān)閉相應(yīng)的檢查。具體的檢查項目可查看這里。https://github.com/markdownlint/markdownlint/blob/master/docs/RULES.md
1.4 小結(jié)
上述內(nèi)容都屬于風格紀律問題,PouchContainer 將編碼規(guī)范檢測自動化,集成到每一次的代碼審閱中,幫助審閱者發(fā)現(xiàn)潛在的問題。
2. 如何編寫 golang 的單元測試
單元測試可用來保證單一模塊的正確性。在測試領(lǐng)域的金字塔里,單元測試覆蓋面越廣,覆蓋功能越全,它就越能減少集成測試以及端到端測試所帶來的調(diào)試成本。在復(fù)雜的系統(tǒng)里,任務(wù)處理的鏈路越長,定位問題的成本就越高,尤其是小模塊所引發(fā)的問題。接下來的內(nèi)容會分享 PouchContainer 編寫 golang 單元測試用例的總結(jié)。
2.1 Table-Driven?Test - DRY
簡單地理解單元測試是給定某一個函數(shù)既定的輸入,判斷是否能得到預(yù)期的輸出。當被測試的函數(shù)有各式各樣的輸入場景時,我們可以采用 Table-Driven 的形式來組織我們的測試用例,如接下來的代碼所示。Table-Driven 采用數(shù)組的方式來組織測試用例,并通過循環(huán)執(zhí)行的方式來驗證函數(shù)的正確性。
//?from?https://golang.org/doc/code.html#Testing package?stringutilimport?"testing"func?TestReverse(t?*testing.T)?{cases?:=?[]struct?{in,?want?string}{{"Hello,?world",?"dlrow?,olleH"},{"Hello,?世界",?"界世?,olleH"},{"",?""},}for?_,?c?:=?range?cases?{got?:=?Reverse(c.in)if?got?!=?c.want?{t.Errorf("Reverse(%q)?==?%q,?want?%q",?c.in,?got,?c.want)}} }為了方便調(diào)試和維護測試用例,我們可以加入一些輔助信息來描述當前的測試。比如 reference ?想要測試 punycode 的輸入時,如果不加入?punycode?的字樣,對于代碼審閱者或者項目維護者而言,他們可能不知道?xn--bcher-kva.tld/redis:3?和?docker.io/library/redis:3?之間的區(qū)別。
{name:??"Normal",input:?"docker.io/library/nginx:alpine",expected:?taggedReference{Named:?namedReference{"docker.io/library/nginx"},tag:???"alpine",},err:?nil, },?{name:??"Punycode",input:?"xn--bcher-kva.tld/redis:3",expected:?taggedReference{Named:?namedReference{"xn--bcher-kva.tld/redis"},tag:???"3",},err:?nil, }但是有些函數(shù)行為比較復(fù)雜,一次輸入并不能作為一次完整的測試用例。例如 TestTeeReader , TeeReader 從 buffer 里讀出?hello, world?之后,已經(jīng)將數(shù)據(jù)讀取完畢了,如果再去讀取,預(yù)期的行為是會遇到 end-of-file 的錯誤。這樣的測試用例需要單獨一個 case 來完成,不需要硬湊出 Table-Driven 的形式。
簡單來說,如果你測試某一個函數(shù)需要拷貝大部分代碼時,理論上這些測試代碼都可以抽出來,并使用 Table-Driven 的方式來組織測試用例 Don‘t Repeat Yourself 是我們遵守的原則。
NOTE: Table-Driven 組織方式是 golang 社區(qū)所推薦,詳情請查看這里。
https://github.com/golang/go/wiki/TableDrivenTests
2.2 Mock - 模擬外部依賴
在測試過程經(jīng)常會遇到依賴的問題,比如 PouchContainer client 需要 HTTP server ,但這對于單元而言太重,而且這屬于集成測試的范疇。那么該如何完成這部分的單元測試呢?
在 golang 的世界里,interface 的實現(xiàn)屬于 Duck Type 。某一個接口可以有各式各樣的實現(xiàn),只要實現(xiàn)能符合接口定義。如果外部依賴是通過 interface 來約束,那么單元測試里就模擬這些依賴行為。接下來的內(nèi)容將分享兩種常見的測試場景。
2.2.1 RoundTripper
還是以 PouchContainer client 測試為例。PouchContainer client 所使用的是 http.Client。其中 http.Client 中使用了 RoundTripper 接口來執(zhí)行一次 HTTP 請求,它允許開發(fā)者自定義發(fā)送 HTTP 請求的邏輯,這也是 golang 能在原有基礎(chǔ)上完美支持 HTTP 2 協(xié)議的重要原因。
http.Client?->?http.RoundTripper?[http.DefaultTransport]對于 PouchContainer client 而言,測試關(guān)注點主要在于傳入目的地址是否正確、傳入的 query 是否合理,以及是否能正常返回結(jié)果等。因此在測試之前,開發(fā)者需要準備好對應(yīng)的 RoundTripper 實現(xiàn),該實現(xiàn)并不負責實際的業(yè)務(wù)邏輯,它只是用來判斷輸入是否符合預(yù)期即可。
如接下來的代碼所示,PouchContainer?newMockClient?可接受自定義的請求處理邏輯。在測試刪除鏡像的用例中,開發(fā)者在自定義的邏輯里判斷了目的地址和 HTTP Method 是否為 DELETE,這樣就可以在不啟動 HTTP Server 的情況下完成該有的功能測試。
//?https://github.com/alibaba/pouch/blob/master/client/client_mock_test.go#L12-L22 type?transportFunc?func(*http.Request)?(*http.Response,?error)func?(transFunc?transportFunc)?RoundTrip(req?*http.Request)?(*http.Response,?error)?{return?transFunc(req) }func?newMockClient(handler?func(*http.Request)?(*http.Response,?error))?*http.Client?{return?&http.Client{Transport:?transportFunc(handler),} }//?https://github.com/alibaba/pouch/blob/master/client/image_remove_test.go func?TestImageRemove(t?*testing.T)?{expectedURL?:=?"/images/image_id"httpClient?:=?newMockClient(func(req?*http.Request)?(*http.Response,?error)?{if?!strings.HasPrefix(req.URL.Path,?expectedURL)?{return?nil,?fmt.Errorf("expected?URL?'%s',?got?'%s'",?expectedURL,?req.URL)}if?req.Method?!=?"DELETE"?{return?nil,?fmt.Errorf("expected?DELETE?method,?got?%s",?req.Method)}return?&http.Response{StatusCode:?http.StatusNoContent,Body:???????ioutil.NopCloser(bytes.NewReader([]byte(""))),},?nil})client?:=?&APIClient{HTTPCli:?httpClient,}err?:=?client.ImageRemove(context.Background(),?"image_id",?false)if?err?!=?nil?{t.Fatal(err)} }2.2.2 MockImageManager
對于內(nèi)部 package 之間的依賴,比如 PouchContainer Image API Bridge 依賴于 PouchContainer Daemon ImageManager,而其中的依賴行為由 interface 來約定。如果想要測試 Image Bridge 的邏輯,我們不必啟動 containerd ,我們只需要像 RoundTripper 那樣,實現(xiàn)對應(yīng)的 Daemon ImageManager 即可。
//?https://github.com/alibaba/pouch/blob/master/apis/server/image_bridge_test.go type?mockImgePull?struct?{mgr.ImageMgrhandler?func(ctx?context.Context,?imageRef?string,?authConfig?*types.AuthConfig,?out?io.Writer)?error }func?(m?*mockImgePull)?PullImage(ctx?context.Context,?imageRef?string,?authConfig?*types.AuthConfig,?out?io.Writer)?error?{return?m.handler(ctx,?imageRef,?authConfig,?out) }func?Test_pullImage_without_tag(t?*testing.T)?{var?s?Servers.ImageMgr?=?&mockImgePull{ImageMgr:?&mgr.ImageManager{},handler:?func(ctx?context.Context,?imageRef?string,?authConfig?*types.AuthConfig,?out?io.Writer)?error?{assert.Equal(t,?"reg.abc.com/base/os:7.2",?imageRef)return?nil},}req?:=?&http.Request{Form:???map[string][]string{"fromImage":?{"reg.abc.com/base/os:7.2"}},Header:?map[string][]string{},}s.pullImage(context.Background(),?nil,?req) }2.2.3 小結(jié)
ImageManager 和 RoundTripper 除了接口定義的函數(shù)數(shù)目不同以外,模擬的方式是一致的。通常情況下,開發(fā)者可以手動定義一個將方法作為字段的結(jié)構(gòu)體,如接下來的代碼所示。
type?Do?interface?{Add(x?int,?y?int)?intSub(x?int,?y?int)?int }type?mockDo?struct?{addFunc?func(x?int,?y?int)?intsubFunc?func(x?int,?y?int)?int }//?Add?implements?Do.Add?function. type?(m?*mockDo)?Add(x?int,?y?int)?int?{return?m.addFunc(x,?y) }//?Sub?implements?Do.Sub?function. type?(m?*mockDo)?Sub(x?int,?y?int)?int?{return?m.subFunc(x,?y) }當接口比較大、比較復(fù)雜的時候,手動的方式會給開發(fā)者帶來測試上的負擔,所以社區(qū)提供了自動生成的工具,比如 mockery ,減輕開發(fā)者的負擔。
2.3 其他偏門
有些時候依賴的是第三方的服務(wù),比如 PouchContainer client 就是一個很典型的案例。上文介紹 Duck Type 可以完成該案例的測試。除此之外,我們還可以通過注冊 http.Handler 的方式,并啟動 mockHTTPServer 來完成請求處理。這樣測試方式比較重,建議在不能通過 Duck Type 方式測試時再考慮使用,或者是放到集成測試中完成。
NOTE: golang 社區(qū)有人通過修改二進制代碼的方式來完成 monkeypatch 。這個工具不建議使用,還是建議開發(fā)者設(shè)計和編寫出可測試的代碼。
2.4 小結(jié)
PouchContainer 將單元測試用例集成到代碼審閱階段,審閱者可以隨時查看測試用例的運行情況。
3.?總結(jié)
在代碼審閱階段,應(yīng)該通過持續(xù)集成的方式,將代碼風格檢查、單元測試和集成測試跑起來,這樣才能幫助審閱者作出準確的決定,而目前 PouchContainer 主要通過 TravisCI/CircleCI 和 pouchrobot 來完成代碼風格檢查和測試等操作。
阿里巴巴開源容器 PouchContainer Meetup上海站報名啦!
?
為了分享并促進?PouchContainer?社區(qū)的進步,邀請大家參加 2018 年 9 月 9 日(周日)上海 PouchContainer Meetup,除了現(xiàn)場的分享,我們更期望和大家的深入交流,一起討論關(guān)于容器的未來和想象。
?
時間:2018年9月9日(周日) 13:00 – 18:00
地點:上海市 靜安區(qū) 靜安寺街道 南京西路1649號 靜安公園 8 號樓二樓
?
識別下方二維碼,即可報名。由于場地容納人數(shù)有限,我們會做進一步篩選,請以收到的短信通知為準。
?
總結(jié)
以上是生活随笔為你收集整理的2300+ commits,80+ Contributors,阿里 PouchContainer 工程质量实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 阿里百万级规模开源容器 PouchCon
- 下一篇: 深入浅出 PouchContainer