Go testing 里的巧妙设计
2023-5-12 08:53:38 Author: Go语言中文网(查看原文) 阅读量:10 收藏

阅读本文大概需要 5 分钟。

网络上写testing的文章浩如云烟,有初学者Hello World级别的玩具,也有大佬干货满满的实践。

不跟那个风(其实是写得太水了有失逼格,写得太高深了力有不逮,团队没有测试的传统)。

换个角度来学习一下testing领域巧妙的设计,开阔眼界是为了举一反三,所谓技多不压身。

按照惯例,从一个问题开始:

问题

testingunsafe不一样: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_testbytes是有点关系,但所谓亲兄弟明算账,bytes的私有财产譬如indexBytePortablebytes_test是用不了的。

于是新的问题出现了:**package bytes无法使用testingpackage bytes_test无法访问indexBytePortable,那么bytes的私有元素就不能测试了吗?**

有道是“有困难要上,没有困难制造困难也要上”,沧海横流方显英雄本色。请看官方团队带来的华丽表演,而且至少是2种(如有缺失欢迎补充)。

export_test.go

我们还是以bytes为例。

备注:export_test.go这个名字和go:build ignore里的ignore一样都是约定俗成,并没有强制要求。(蛋是,你不遵守就等着被怼)

  1. 通过export_test.go将私有元素给一个exported的代理

    // export_test.go
    package bytes

    // Export func for testing
    var IndexBytePortable = indexBytePortable

  2. 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导致了间接依赖。

  1. 定义接口

    // 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
    }

  2. 使用接口

    // 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)
     ...
    }

  3. 测试转发

    // 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 ./bytesgo test .等,本质不变)

// 还是以bytes为例,此处的p代表package bytes
func TestPackagesAndErrors(ctx context.Context, done func()opts PackageOptsp *Packagecover *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


文章来源: http://mp.weixin.qq.com/s?__biz=MzAxMTA4Njc0OQ==&mid=2651454342&idx=1&sn=991851875ff5bc7da168dd13f6ed21a7&chksm=80bb2574b7ccac62ebcee570b929f0e2f72cb470f7bd706e749200a8401c57b58ff559e48f3d#rd
如有侵权请联系:admin#unsafe.sh