Go语言的代码测试 | 青训营笔记

164 阅读2分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 8 天

Go语言的测试

回归测试:终端进行测试

集成测试:功能维度的测试,某个接口的测试

单元测试:单独的函数模块进行测试

1 Go语言中的测试规则

  • 所有的测试文件以_test.go结尾;
  • 测试函数为func TestXxx(*testing.T)的形式;
  • 初始化逻辑放到TestMain中;
func Func1() {
    // ...
}

func TestFunc1(t *testing.T) {
    // ...
}

func TestMain(m *testing.M) {
    // 测试前:数据装载、配置初始化等
    code := m.Run()
    // 测试后:释放资源等
    os.Exit(code)
}

2 单元测试

输入 -> 测试单元(函数、模块...)-> 输出

将输出和期望进行校对,保证质量、提升效率。

2.1 assert

获取第三方的assert包

go get -u "github.com/stretchr/testify/assert"

测试SayHello函数

// hello.go
func SayHello() string {
	return "GoodBye!"
}
// hello_test.go
func TestSayHello(t *testing.T) {
	output := SayHello()
	expectOutput := "Hello!"
	assert.Equal(t, expectOutput, output)
}

2.2 覆盖率

测试判断是否及格的函数

// judge.go
func JudgePassLine(score int16) bool {
	if score >= 60 {
		return true
	}
	return false
}
// judge_test.go
func TestJudgePassLine(t *testing.T) {
	isPass := JudgePassLine(70)
	assert.Equal(t, true, isPass)
}

使用命令行进行测试:

go test judge_test.go judge.go --cover
command-line-arguments  0.351s  coverage: 66.7% of statements

添加测试用例使其覆盖率到100%:

// judge_test.go
func TestJudgePassLineTrue(t *testing.T) {
	isPass := JudgePassLine(70)
	assert.Equal(t, true, isPass)
}

func TestJudgePassLineFalse(t *testing.T) {
	isPass := JudgePassLine(50)
	assert.Equal(t, false, isPass)
}

2.3 依赖

一个复杂项目的依赖可能会有文件、数据库、缓存等强依赖。幂等:一个case重复运行很多次它的结果应该是一致的;稳定指单元测试相互隔离,能在任何时间、任何函数进行独立运行。

下面是一个文件的强依赖示例:

// file.go
func ReadFirstLine() string {
	open, err := os.Open("test1.txt")
	defer open.Close()
	if err != nil {
		return ""
	}
	scanner := bufio.NewScanner(open)
	for scanner.Scan() {
		return scanner.Text()
	}
	return ""
}

func ProcessFirstLine() string {
	line := ReadFirstLine()
	dstLine := strings.ReplaceAll(line, "11", "00")
	return dstLine
}
// file_test.go
func TestProcessFirstLine(t *testing.T) {
	firstLine := ProcessFirstLine()
	assert.Equal(t, "line00", firstLine)
}

2.4 Mock

开源Mock测试包 monkey:github.com/bouk/monkey

快速Mock函数:为函数或者方法打桩从而将A函数替换为B函数

下面使用monkey的函数将ReadFirstLine函数替换为打桩的函数:

// 对ReadFirstLine函数进行Mock,不再对本地文件依赖
func TestProcessFirstLineWithMock(t *testing.T) {
	monkey.Patch(ReadFirstLine, func() string {
		return "line110"
	})
	defer monkey.Unpatch(ReadFirstLine)
	line := ProcessFirstLine()
	assert.Equal(t, "line000", line)
}

2.5 单元测试Tips

  • 一般覆盖率:50%~60%,较高覆盖率:80%+;
  • 测试分支相互独立、全面覆盖;
  • 测试单元粒度足够小,函数单一职责。

3. 基准测试

优化代码,需要对当前代码分析,Go语言内置了测试框架提供以基准测试。

// benchmark.go
var ServerIndex [10]int

func InitServerIndex() {
	for i := 0; i < 10; i++ {
		ServerIndex[i] = i + 100
	}
}

func Select() int {
	return ServerIndex[rand.Intn(10)]
}
// benchmark_test.go
func BenchmarkSelect(b *testing.B) {
    // 串行测试
	InitServerIndex()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		Select()
	}
}

func BenchmarkSelectParallel(b *testing.B) {
    // 并行测试
	InitServerIndex()
	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			Select()
		}
	})
}

rand函数为了保证全局随机性和并发安全,降低了并发的性能,因此结果上看Select函数的并行性能不如串行的性能。

优化方法:

// go get -u "github.com/bytedance/gopkg"
func FastSelect() int {
	return ServerIndex[fastrand.Intn(10)]
}