阅读本文大概需要 5 分钟。
网络上写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种(如有缺失欢迎补充)。
我们还是以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_testimport (
. "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)
}
}
}
这次以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_testimport (
. "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
,它比第一种方案更具普适性(在测试这个特定场景下也更繁琐),完全可以在真实的世界里发挥巨大作用。
我是 polarisxu,北大硕士毕业,曾在 360 等知名互联网公司工作,10多年技术研发与架构经验!2012 年接触 Go 语言并创建了 Go 语言中文网!著有《Go语言编程之旅》、开源图书《Go语言标准库》等。
坚持输出技术(包括 Go、Rust 等技术)、职场心得和创业感悟!欢迎关注「polarisxu」一起成长!也欢迎加我微信好友交流:gopherstudio