Go testing 裏的巧妙設計
網絡上寫testing
的文章浩如雲煙,有初學者Hello World
級別的玩具,也有大佬乾貨滿滿的實踐。
不跟那個風(其實是寫得太水了有失逼格,寫得太高深了力有不逮,團隊沒有測試的傳統)。
換個角度來學習一下testing
領域巧妙的設計,開闊眼界是爲了舉一反三,所謂技多不壓身。
按照慣例,從一個問題開始:
問題
testing
和unsafe
不一樣:unsafe
所有的語義和實現都由編譯器負責解釋,本身只是作爲佔位符提供文檔;testing
雖然是用來測試其它package
的,但是它本身並沒有在編譯器那裏得到任何優待,只是普通的一個package
,它自己也需要依賴其它package
也需要測試。
testing
支持的場景越複雜,引入的依賴就越多。譬如:bytes
flag
fmt
io
os
reflect
runtime
sync
等等還有很多。
那麼問題來了:以bytes
爲例,既然testing
依賴於bytes
,那麼bytes
在測試時依賴testing
不就import cycle
了嗎?
這個問題難度較小。testing
裏有如下描述:
If the test file is in the same package, it may refer to unexported identifiers within the package.
If the file is in a separate "_test" package, the package being tested must be imported explicitly and only its exported identifiers may be used. This is known as "black box" testing.
也就是說bytes/xxx_test.go
裏面既可以是package bytes
也可以是package bytes_test
,所謂的白盒測試和黑盒測試。既然前者不能import "testing"
那麼全部採用後者不就解決了嗎?
完美!世界從此太平了
嗎?並沒有。因爲bytes_test
和bytes
是有點關係,但所謂親兄弟明算賬,bytes
的私有財產譬如indexBytePortable
,bytes_test
是用不了的。
於是新的問題出現了:package bytes
無法使用testing
,package bytes_test
無法訪問indexBytePortable
,那麼bytes
的私有元素就不能測試了嗎?
有道是 “有困難要上,沒有困難製造困難也要上”,滄海橫流方顯英雄本色。請看官方團隊帶來的華麗表演,而且至少是 2 種(如有缺失歡迎補充)。
export_test.go
我們還是以bytes
爲例。
備註:export_test.go
這個名字和go:build ignore
裏的ignore
一樣都是約定俗成,並沒有強制要求。(蛋是,你不遵守就等着被懟)
-
通過
export_test.go
將私有元素給一個exported
的代理// export_test.go package bytes // Export func for testing var IndexBytePortable = indexBytePortable
-
在
package bytes_test
使用該代理替代原私有元素// bytes_test.go package bytes_test import ( . "bytes" ... ) func TestIndexByte(t *testing.T) { for _, tt := range indexTests { ... a := []byte(tt.a) b := tt.b[0] // IndexByte是公開的,使用沒有限制。 pos := IndexByte(a, b) if pos != tt.i { t.Errorf(`IndexByte(%q, '%c') = %v`, tt.a, b, pos) } // bytes_test中不能直接使用indexBytePortable,自然無法測試indexBytePortable。 // 爲了達到“不可告人的目的”,把indexBytePortable改寫成了IndexBytePortable。 posp := IndexBytePortable(a, b) if posp != tt.i { t.Errorf(`indexBytePortable(%q, '%c') = %v`, tt.a, b, posp) } } }
XTest
這次以context
爲例。
和bytes
稍有不同,testing
並不直接依賴context
,但是testing/internal/testdeps
依賴context
導致了間接依賴。
-
定義接口
// context_test.go package context // 參照testing.T一比一設計 type testingT interface { Deadline() (time.Time, bool) Error(args ...any) Errorf(format string, args ...any) Fail() FailNow() Failed() bool Fatal(args ...any) Fatalf(format string, args ...any) Helper() Log(args ...any) Logf(format string, args ...any) Name() string Parallel() Skip(args ...any) SkipNow() Skipf(format string, args ...any) Skipped() bool }
-
使用接口
// context_test.go // 注意這裏不是context_test package context // 注意XTestXxx並不能被go test識別 func XTestParentFinishesChild(t testingT) { ... parent, cancel := WithCancel(Background()) cancelChild, stop := WithCancel(parent) defer stop() valueChild := WithValue(parent, "key", "value") timerChild, stop := WithTimeout(valueChild, veryLongDuration) defer stop() afterStop := AfterFunc(parent, func() {}) defer afterStop() // 由於testingT是參照testing.T設計的,因此testing.T怎麼使用,在此也可以怎麼使用 select { case x := <-parent.Done(): t.Errorf("<-parent.Done() == %v want nothing (it should block)", x) case x := <-cancelChild.Done(): t.Errorf("<-cancelChild.Done() == %v want nothing (it should block)", x) case x := <-timerChild.Done(): t.Errorf("<-timerChild.Done() == %v want nothing (it should block)", x) case x := <-valueChild.Done(): t.Errorf("<-valueChild.Done() == %v want nothing (it should block)", x) default: } // 注意這裏用到了cancelCtx和timerCtx,它們都是私有的,只能放在package context文件裏。 pc := parent.(*cancelCtx) cc := cancelChild.(*cancelCtx) tc := timerChild.(*timerCtx) ... }
-
測試轉發
// x_test.go // 注意這裏是context_test package context_test import ( . "context" "testing" ) // context_test和testing並不構成循環依賴關係,因此可以使用標準的testing簽名 func TestParentFinishesChild(t *testing.T) { // testingT是參照testing.T設計的,因此*testing.T天熱滿足testingT接口。 // 在此通過接口就避免了循環依賴的問題。 XTestParentFinishesChild(t) // uses unexported context types }
思考
兩種方案都介紹完了,但事情並沒有結束。
封裝
先解釋一個問題:indexBytePortable
一番折騰成了IndexBytePortable
,那麼indexBytePortable
私有不私有還有什麼意義?
也許會有人說:簡單得不值一提嘛,它在_test.go
文件裏,編譯的時候根本就忽略了。
怎麼說呢?沒有錯,但離正確也還遠。
既然go build
的時候不讓用,那麼在go test foo
測試的時候用一用總可以吧?
仍然不可以,因爲bytes/export_test.go
只有go test bytes
的時候才發光發熱。
(寫法可以有其它種,譬如go test ./bytes
、go test .
等,本質不變)
// 還是以bytes爲例,此處的p代表package bytes
func TestPackagesAndErrors(ctx context.Context, done func(), opts PackageOpts, p *Package, cover *TestCover) (pmain, ptest, pxtest *Package) {
...
if len(p.TestGoFiles) > 0 || p.Name == "main" || cover != nil && cover.Local {
/*
.go文件從大的方向分三種:
1. 普通編譯使用的文件(譬如bytes.go),細分爲以下幾種:
+ 使用了import "C"的 ==========> CgoFiles
+ 有語法錯誤的 ================> InvalidGoFiles
+ 因爲os/arch/tag等原因排除的 ==> IgnoredGoFiles
+ 通常意義理解的,其實是GoFiles
2. 測試文件(譬如example_test.go),又分兩種:
+ 如果和GoFiles屬於同一個package === > TestGoFiles
+ 如果是package xxx_test ===========> XTestGoFiles
3. 編譯器認爲應該忽略的文件,分爲以下幾種:
+ 以_和.開頭的文件
*/
ptest = new(Package)
*ptest = *p
ptest.Error = ptestErr
ptest.Incomplete = incomplete
ptest.ForTest = p.ImportPath
ptest.GoFiles = nil
// 普通編譯使用的文件,編譯這些文件不值得大驚小怪。
ptest.GoFiles = append(ptest.GoFiles, p.GoFiles...)
// 測試文件中的第一類,也需要參與編譯。
// 這是IndexBytePortable能夠被測試的保證,但是注意只是在測試場景下。
// 這裏的p就是要測試的目標,那麼只有go test bytes的時候,bytes/export_test.go才參與編譯。
ptest.GoFiles = append(ptest.GoFiles, p.TestGoFiles...)
ptest.Target = ""
ptest.Imports = str.StringList(p.TestImports, p.Imports)
ptest.Internal.Imports = append(imports, p.Internal.Imports...)
ptest.Internal.RawImports = str.StringList(rawTestImports, p.Internal.RawImports)
ptest.Internal.ForceLibrary = true
ptest.Internal.BuildInfo = ""
ptest.Internal.Build = new(build.Package)
*ptest.Internal.Build = *p.Internal.Build
...
}
}
接口
說起接口的作用,人們首先想起的總是那些八股文。
但是在這裏,官方團隊給我們上了生動的一課:接口在解決循環依賴中的巨大作用。
而且這種手法並不侷限於context
或者testing
,它比第一種方案更具普適性(在測試這個特定場景下也更繁瑣),完全可以在真實的世界裏發揮巨大作用。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/wveQsvB81MEvG58vgwEgRQ