Go 中的進階測試模式

Go 使編寫測試非常簡單。實際上,測試工具是內置在標準工具鏈裏的,你可以簡單地運行 go test 來運行你的測試,無需安裝任何額外的依賴或任何別的東西。測試包是標準庫的一部分,我很高興地看到它的使用範圍非常廣泛。

當你在使用 Go 編寫服務實現時,希望你的測試覆蓋率隨着時間的推移而增長。隨着測試範圍的擴大,測試運行時間也會變長。你希望用服務集成及集成測試來測試服務的重要部分。你發現在某些情況下,集成測試和各種公共服務的耦合對 CI 和開發產生限制。

集成測試

我是集成測試的忠實信徒。有人可能無法直接看到它的好處,但對於 LTS(長期支持)的版本,進行集成測試是一個很好的主意,因爲你顯然想要隨着時間的推移升級你的服務。如果你要從 MySQL 5.7 切換到 8.0 (甚至是換成 PostgreSQL),你需要合理地確保你的服務依然正常工作,然後你可以檢測問題並根據需要對實現進行更新。

集成測試最近對我有用的一個例子是檢測到 MySQL 保留字增加的情況:我有一個數據庫部署裏用到了 rank 字段。這個詞在 MySQL 5.7 及之前是可以使用的,但在 MySQL 8.0 裏它變成了一個保留字。集成測試捕獲了這個問題,而模擬(mock)則無法做到這一點。

RANK ®; 在 8.0.2 版增加 (爲保留字) 見 MySQL 8.0 的關鍵詞和保留字 [1]

模擬是單元測試的一種擴展,而由於集成測試可能意味着高昂的成本,今天做集成測試比過去容易得多。隨着 Docker 的不斷髮展,並有了像 Drone CI[2] 這樣 Docker-first 的 CI,我們可以在 CI 測試套件裏聲明我們的服務。讓我們看一下我定義的 MySQL 服務:

services:
- name: crust-db
  image: percona:8.0
  ports:
 - 3306
  environment:
 MYSQL_ROOT_PASSWORD: bRxJ37sJ6Qu4
 MYSQL_DATABASE: crust
 MYSQL_USER: crust
 MYSQL_PASSWORD: crust

這基本上就是隨我們的測試和構建一起開啓數據庫所需的全部。雖然在過去,這可能意味着你需要一個一直在線的數據庫實例,你需要在某處進行管理,而今天大門已經打開,基本上你可以在你所用的 CI 框架裏聲明服務的一切所需。

["Go 以及集成測試: 使用 Drone CI #golang" via @TitPetric](http://twitter.com/intent/tweet?url=https%3a%2f%2fscene-si.org%2f2019%2f04%2f15%2fnext-level-go-testing%2f&text=%22Go%20and%20integration%20tests%3a%20simple%20with%20Drone%20CI%20%23golang%22&via=TitPetric ""Go 以及集成測試: 使用 Drone CI #golang" via @TitPetric")

我有點跑題了,但這裏的學問是 - 如果你可以避免模擬一些東西,尤其是在你掌控下的服務,一定要考慮編寫集成測試。你無需藉助使用 go-get 獲取的像 gomock[3] 或 moq[4] 這樣的項目。模擬一切是不明智的(例如,net.Conn 不需要模擬,它足夠簡單,可以在你的測試中創建你自己的 client/server,它將存在於內存中)。

實際上,在集成測試和模擬之間也有中間立場,你可以編寫像 Redis 這樣的簡單外部服務的 fake 實現,但你仍然不能捕捉到真實服務的所有細微之處。基本上,只滿足你用到的簡單接口大大降低了實現面(implementation surface),這就只需實現你用到的 API 子集的行爲。

測試範圍(testing surface area)

我正在開發一個項目,目前有 53 個測試文件,其中 28 個是需要外部服務(例如上述的數據庫)的集成測試。你可能並不總是處理完整的環境,或者可能只對在項目中分散的一小部分測試感興趣,並且你希望能夠運行這些(且只運行這些)。

查看 testing 包的 API 面(API surface),我們注意到有一個 Short()[5] 函數可用,它在運行 Go test 時對 -test.short 起作用。這使得我們在想運行測試的某個子集時可以跳過一些測試:

func TestTimeConsuming(t *testing.T) {
 if testing.Short() {
  t.Skip("skipping test in short mode.")
 }
 ...
}

從紙面上看,這意味着你在以 short 模式運行時可以跳過集成測試。但即使從上面的例子也可以看出,當測試持續時間是個重要因素時可以用來跳過某些測試,這纔是動機 —— 實際上,這個應該僅適用於基準測試

那麼,當考慮到你需要顯式地以 -bench 參數啓用基準測試時,你可能會琢磨一個基準與另一個基準測試能否比較快慢。Go 已經很聰明,它默認限制了每個基準運行的時間,而是否要修改這個配置,以及是否想同時使用 short 模式和基準,都由你來決定 - 對我來說,兩個選項同時使用毫無意義。

事實上,short 測試標記不應該用來跳過集成測試。它的目的是加速構建過程,但是代碼判斷或人爲地判斷哪個測試應該是 short 或是 long 讓人望而卻步。強調:要麼運行所有的基準測試要麼不運行。隨着測試集的增長,short 測試標記無法給我們所需的靈活性,所以我們需要一種更具聲明性的方式來界定我們需要運行哪些類型的測試。

更好的方式

現在,傳統觀點會說 “運行所有的測試”。作爲真正瞭解人們如何處理問題,提出問題以及確立實踐準則的工程師中的一個 —— 現在,這可以幫助你找到一個更好的解決辦法,解決並不只是你遇到的問題。

在 2016 年,以及 2016 年晚些時候,Peter Bourgon 寫了兩篇極好的長篇幅文章,這些文章對需要實現實際服務並超出基本實現的人來說,是參考書一樣的存在:

在 2014 年的文章裏,Peter 建議使用構建標記來引入有價值的測試習慣:

包測試主要針對單元測試,但對集成測試來說,事情有點棘手。啓動外部服務的過程通常依賴於你的集成環境,不過我們確實找到了一個針對它們進行集成測試的好習慣。寫一個 integration_test.go 並給它一個 integration 的構建標記。爲服務地址以及連接字符串等定義(全局的)flag,並在測試中使用它們。

事實上,Peter 建議使用 Go 的構建標記來標識集成測試。如果你需要一個單獨的環境來運行這些測試,你只要使用 -tags=intergration 作爲 Go test 的參數。

這完全合乎情理 —— 儘管我在的這個項目的集成測試需要花費一分鐘左右,但我知道在有的項目裏需要幾個小時。這些項目可能有很特殊的專用測試設置,這樣你也可以不測試這些服務的配置 —— 它們只在測試環境中使用。

我很想知道他的觀點在 2014 到 2016 年是否發生了什麼變化。如果有的話,作者會深入研究各種非標準測試包如何成爲他們的 DSL(領域特定語言)。但是經驗是一位好老師,他並沒有對一個 http.Client 進行測試,並指出你不想測試請求進入的 HTTP transport 或正在寫文件的磁盤上的路徑。

在單元測試中你應該專注於業務邏輯,並且通過集成測試,您將驗證集成服務的功能,而不是標準庫或第三方軟件包如何實現集成。

["Go 測試: 哪個適合你 - 單元測試還是集成測試? #golang" via @TitPetric](http://twitter.com/intent/tweet?url=https%3a%2f%2fscene-si.org%2f2019%2f04%2f15%2fnext-level-go-testing%2f&text=%22Go%20testing%3a%20which%20one%20are%20right%20for%20you%20-%20unit%20or%20integration%20tests%3f%20%23golang%22&via=TitPetric ""Go 測試: 哪個適合你 - 單元測試還是集成測試? #golang" via @TitPetric")

邊界情況

將你的應用程序與第三方服務集成是很常見的,由於 API 棄用是可能發生的,所以集成測試可能還需要驗證應用程序的響應是否仍然有意義。因此,Peter 的文章需要一點改進。

你不能總是依賴你正在使用的 API;它會在未來幾年都保持原樣嗎?沒有人希望你創建一堆 GitHub 用戶和組織來測試你的 webhook 端點和集成,但這並不意味着你不會偶爾需要這樣做。

一個最近的例子是 larger deprecation of Bitbucket APIs due to GDPR[8]. 這篇棄用通知是在大約一年前宣佈的, 從 10 月開始,並計劃在 2019 年 4 月底廢棄各種 API 及返回的數據,可能會對現有的各種 CI 集成造成嚴重破壞。

考慮到這一點,我這樣擴展了 Peter 的建議:

我們的測試通常屬於單元測試、集成測試或外部測試的某一類,或者是它們的某種組合。我們肯定希望在 CI 任務中跳過 external 測試,原因顯而易見,但如果我們正在考慮調試開發中的一些相關問題,它們是非常有價值的。我們經常需要定位到具體包中具體的測試,因此運行類似下面的內容是有意義的:

go test --tags="integration external" ./messaging/webhooks/...

根據你的構建標記,這可能會運行你的代碼庫某個子集裏面的所有集成和外部測試,跳過單元測試,或者它可能只運行那個既是集成測試也是外部測試的測試。無論哪種方式,你都專注於包實現,尤其是該包中與提供的標記匹配的所有測試的子集。

對於 CI 任務,範圍確定爲:

go test --tags="unit integration" ./...

這樣,你可以完整地測試所有集成測試,以及完整的包範圍。我們將跳過可能導致我們的 CI 構建失敗的 externalintegration AND external 測試,不讓它們成爲構建的問題。可能每月有那麼一天,GitHub 或 Bitbucket 是壞的,我們只能一直看着它們的狀態頁面。

因此,基本上,除了將某些測試標記爲 integration 之外,我們還希望將其他標記爲 unitexternal ,以便我們可以根據需要在開發環境中跳過它們。沒有人喜歡運行完整的測試集,並且發現它僅僅因爲 GitHub 出問題而失敗。而具有開發和調試目的的選項是非常寶貴的。

對測試進行測試

在重構測試時,你經常只會在運行測試時才發現,有些符號或其他東西已經不存在了,導致你的測試無法編譯。這個問題的一個好的解決辦法是僅僅針對測試文件的編譯步驟進行測試。有一些東西可以發揮作用:

  1. 可以通過給 go test 填寫 -run 參數來跳過測試。你可以運行 go test -run=^$ ./...,它將有效地編譯你的完整測試集並跳過所有測試。這對於運行時間較長的 CI 任務非常有用,因爲它實際上是一個編譯時的檢查,確保所有測試都是可運行的。但是,這仍然會運行你的 TestMain 函數。

  2. Go 1.10 引入了 -failfast  標誌。如果你的某些測試失敗而你有一個非常大的測試集,那麼在錯誤 / 失敗之間會有很多輸出,在其他測試完成之前,以及通知你失敗之前也會有很多。使用此選項,你可以對這個問題稍做優化,代價是同一測試集中在之後運行的測試中可能還會有失敗的。這是測試所有內容和報告所有錯誤,或僅在發現第一個錯誤之前進行測試的區別。

  3. -failfast 標誌對 ./ ... 沒有任何作用,例如,如果其中一個包由於編譯錯誤而失敗,它將繼續針對剩餘的檢測到的包進行測試。

這些基本上是圍繞 golang/go#15535[9] 的問題,實際上這意味着我們無法像使用 go build 一樣只針對測試的編譯進行測試。

> Go 測試:編譯時檢查你的測試而無需運行它們 #protip #golang via @TitPetric[10]

公有以及私有測試 API

理想情況下,你將對你的包進行黑盒測試。這意味着若你的包起名爲 store,你的測試就會在 store_test 包中。這可能爲你解決了這樣一個依賴問題,即 url 包依賴 http 包,而反過來也存在依賴。使用 url_testhttp_test 包 解決了這個問題 [11]。

此外,有一些適用於任何代碼庫的準則:

特別地,對於名爲 store 的東西,你應該有:

有一些關於如何使用這些準則的例子。在 Michael Hashimoto 的一次題爲高級的 Go 測試的演講裏,他主張測試作爲公共 API[12]。

對於此我有一個問題(也許不是一個特別相關的問題),對公共 API 的任何修改都需要有某種兼容性保證。儘管這本身是可以接受的,但它並不是一個明確的規範。在大多數情況下,將這些測試函數限定在當前項目測試的範圍內是更容易接受的。

我會只是將這些函數添加在 store_internal_test.go 中 —— 在 *_test.go 中定義的任意公共標識符在包中依然是可用的,不過只能在測試中訪問。當你的應用被編譯時,不會去拉取你在測試文件中聲明的任何東西。當你改變主意,需要將其中一些變爲公共的 —— 你只需將相關代碼移動到 testing.go 文件中,而不需要修改任意一行測試代碼。

“Go 測試:你是否應該用公共 API 提供測試所需設施?#golang” via @TitPetric[13]

以上建議的原則也適用於從包中對外暴露一些私有符號,以便在黑盒測試中使用。我似乎無法找到一個強有力的例子來證明這種方法的合理性,除開上面講到的循環引用的問題,從你的包中對外暴露內部的東西然後只用於你的測試是可以做到的。但如果沿這條路走下去,你實際上是將內部測試和黑盒測試混在一起,我建議你不要這麼做。內部的東西會變化,導致你的測試也變得更脆弱。

在這片荒野之地很少有例子,不過還是有一些:

實際上,在大多數情況下人們可以編寫內部測試來實現相同的目標。我並不是在提倡,尤其是這樣的 _internal_test.go 文件應該將內部暴露給黑盒測試,但是我看到了使用它們來提供有一天可能成爲公共包 API 的效用實體,是有意義的。這仍然是太大的一步,但這一切都取決於你的需求。如果你不希望在給定日期或給定版本之前發佈某部分 API,可以採取這種方式,對於每個 API,可以將其實現爲公共包 API,而無需真正對外發布供測試之外使用。

既然我已經把你帶到了這裏……

如果你買我的一本書會很棒:

我保證如果買了你會學到更多。購買副本支持我寫更多關於類似主題的內容。謝謝你,請買我的書。

如果想預定我的顧問 / 自由職業服務時間,請隨時給我發郵件 [19]。我對 API,Go,Docekr,VueJS 以及擴展服務等等 [20] 都很在行。


via: https://scene-si.org/2019/04/15/next-level-go-testing/

作者:Tit Petric[21] 譯者:krystollia[22] 校對:polaris1119[23]

本文由 GCTT[24] 原創編譯,Go 中文網 [25] 榮譽推出,發佈在 Go 語言中文網公衆號,轉載請聯繫我們授權。

參考資料

[1]

MySQL 8.0 的關鍵詞和保留字: https://dev.mysql.com/doc/refman/8.0/en/keywords.html

[2]

Drone CI: https://drone.io/

[3]

gomock: https://github.com/golang/mock

[4]

moq: https://github.com/matryer/moq

[5]

Short(): https://golang.org/pkg/testing/#Short

[6]

Go best practices for production environments (2014): https://peter.bourgon.org/go-in-production/

[7]

Go best practices, six years in (2016): https://peter.bourgon.org/go-best-practices-2016/

[8]

larger deprecation of Bitbucket APIs due to GDPR: https://developer.atlassian.com/cloud/bitbucket/bbc-gdpr-api-migration-guide/

[9]

golang/go#15535: https://github.com/golang/go/issues/15513

[10]

Go 測試:編譯時檢查你的測試而無需運行它們 #protip #golang via @TitPetric: http://twitter.com/intent/tweet?url=https%3a%2f%2fscene-si.org%2f2019%2f04%2f15%2fnext-level-go-testing%2f&text=%22Go%20testing%3a%20compile-time%20check%20your%20tests%20without%20running%20them%20%23protip%20%23golang%22&via=TitPetric

[11]

解決了這個問題: https://talks.golang.org/2014/testing.slide#21

[12]

測試作爲公共 API: https://about.sourcegraph.com/go/advanced-testing-in-go#testing-as-a-public-api

[13]

“Go 測試:你是否應該用公共 API 提供測試所需設施?#golang” via @TitPetric: http://twitter.com/intent/tweet?url=https%3a%2f%2fscene-si.org%2f2019%2f04%2f15%2fnext-level-go-testing%2f&text=%22Go%20testing%3a%20Should%20you%20have%20a%20public%20API%20to%20provide%20testing%20utilities%3f%20%23golang%22&via=TitPetric

[14]

API Testing - Swagger: https://goswagger.io/faq/faq_testing.html

[15]

Separate _test package: https://segment.com/blog/5-advanced-testing-techniques-in-go/

[16]

Export unexported method for test: https://medium.com/@robiplus/golang-trick-export-for-test-aa16cbd7b8cd

[17]

API Foundations in Go: https://leanpub.com/api-foundations

[18]

12 Factor Apps with Docker and Go: https://leanpub.com/12fa-docker-golang

[19]

給我發郵件: black@scene-si.org

[20]

等等: https://scene-si.org/about

[21]

Tit Petric: https://scene-si.org/about

[22]

krystollia: https://github.com/krystollia

[23]

polaris1119: https://github.com/polaris1119

[24]

GCTT: https://github.com/studygolang/GCTT

[25]

Go 中文網: https://studygolang.com/

福利

我爲大家整理了一份從入門到進階的 Go 學習資料禮包,包含學習建議:入門看什麼,進階看什麼。關注公衆號 「polarisxu」,回覆 ebook 獲取;還可以回覆「進羣」,和數萬 Gopher 交流學習。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/bVBX5sYbd32Il7IuL0waTw