go 语言 外部测试包

外部测试包

在测试代码的时候,会遇到包循环依赖导入的问题,Go语言规范是禁止包的循环依赖,为了解决这个问题,可以引入外部包。

外部包使用package xxx_test方式来命名,比如package bytes_test就是src/btyes/buffer外部包的命名方式,_test后缀告诉go test工具它应该建立一个额外的包来运行测试。

package xxx_test中的xxx_test并不需要真的创建这样一个目录,而是在xxx包下的测试文件中引入这个包名就可以了。btyes包的源码目录结构如下

/src/bytes
├── boundary_test.go
├── buffer.go
├── buffer_test.go
├── bytes.go
├── bytes_test.go
├── compare_test.go
├── example_test.go
├── export_test.go
├── reader.go
└── reader_test.go

通过外部测试包的方式可以解决导入包循环依赖的问题,因为外部测试包是一个独立的包,所以能够导入那些依赖待测代码本身的其他辅助包;包内的测试代码就无法做到这点。注意:外部包不能被其它包导入

// 文件 src/btyes/buffer_test.go
package bytes_test // 'btyes目录下并不存在 bytes_test 目录'
import (
	. "bytes"
	... 代码片段
)

func TestNewBuffer(t *testing.T) {
	buf := NewBuffer(testBytes)
	check(t, "NewBuffer", buf, testString)
}

... 代码片段
func empty(t *testing.T, testname string, buf *Buffer, s string, fub []byte) {}

go list 命令

  • 通过GoFiles查看bytes包的Go源文件

    go list -f={{.GoFiles}} bytes // => [buffer.go bytes.go reader.go]
  • 通过TestGoFiles查看bytes包内部的测试代码

    go list -f={{.TestGoFiles}} bytes // => [export_test.go]
  • 通过XTestGoFiles查看bytes包的外部测试代码,也就是bytes_test包

    go list -f={{.XTestGoFiles}} bytes
    // => [buffer_test.go bytes_test.go compare_test.go example_test.go reader_test.go]

访问内部代码

如果在测试中,需要对包内部的没有导出的函数进行测试,可以利用包内的 _test.go文件,如 export_test.go,在这个文件中将包的内部函数、方法导出,以供外部测试包使用。

indexBytePortable方法在 src/bytes/bytes.go 中定义

// src/bytes/bytes.go
func indexBytePortable(s []byte, c byte) int {/* ... */} // 这是一个内部方法

bytes包中的内部方法导出,供外部包package bytes_test使用。因为只有在测试时,才会把内部代码导出,所以导出内部代码是安全的

// src/bytes/export_test.go
package bytes
// Export func for testing
var IndexBytePortable = indexBytePortable // 赋值给包级可导出变量 IndexBytePortable

外部包使用导出的方法IndexBytePortable

// src/bytes/bytes_test.go
package bytes_test

func TestIndexByte(t *testing.T) {
	for _, tt := range indexTests {
		... 代码片段
		posp := IndexBytePortable(a, b) // 导出的内部方法在这里被使用
		if posp != tt.i {
			t.Errorf(`indexBytePortable(%q, '%c') = %v`, tt.a, b, posp)
		}
	}
}

测试文件export_test.go并没有定义测试代码,它只是通过bytes.IndexBytePortable简单导出了内部的indexBytePortable函数,这个技巧可以广泛用于位于外部测试包的白盒测试

示例:包循环依赖

目录结构

gott
├── go.mod
├── hi
│   └── hi.go
├── pprint
│   ├── pprint.go
│   └── pprint_test.go

hi.go 文件,引用了 pprint 包

package hi

import (
	"fmt"
	"gott/pprint"
)

func Say() {
	pprint := pprint.PPrint()
	fmt.Println(pprint)
}

pprint.go 文件

package pprint

func PPrint() string {
	return "I'm PPrint()"
}

pprint_test.go 测试文件,在 pprint 包中引入了 hi 包,而 hi 包中已经引入了 pprint 包,这就导致了 包循环依赖

package pprint

import (
	"gott/hi"
	"testing"
)

func TestPPrint(t *testing.T) {
	PPrint()
	hi.Say()
	t.Log("expect call PPrint")
}

执行测试,提示 import cycle not allowed in test

go test -v gott/pprint
# gott/pprint
package gott/pprint (test)
        imports gott/hi
        imports gott/pprint: import cycle not allowed in test
FAIL    gott/pprint [setup failed]
FAIL

修改 pprint_test.go 测试文件,使用外部测试包 pprint_test

package pprint_test

import (
	"gott/hi"
	// 导入 要进行测试的 pprint 包本身
	"gott/pprint"
	"testing"
)

func TestPPrint(t *testing.T) {
	pprint.PPrint()
	hi.Say()
	t.Log("expect call PPrint")
}

"gott/pprint":导入 要进行测试的 pprint 包本身,没有引入外部包时,不需要导入被测试的包本身

执行测试,测试通过

go test -v gott/pprint
=== RUN   TestPPrint
I'm PPrint()
    pprint_test.go:12: expect call PPrint
--- PASS: TestPPrint (0.00s)
PASS
ok      gott/pprint     (cached

使用 Go 官方的代码风格:pprint_test.go 文件,因为pprint_test在 pprint 目录下,通过在 import 时,使用 . 选项,可以直接调用PPrint()方法,这使得调用包内的方法和内部测试包一致,整体代码风格得到统一

package pprint_test

import (
	"gott/hi"
	. "gott/pprint"
	"testing"
)

func TestPPrint(t *testing.T) {
	PPrint()
	hi.Say()
	t.Log("expect call PPrint")
}