Go语言工程实践之测试 | 青训营笔记

86 阅读4分钟

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

前言

在前一篇文章中,我们介绍了 Go 依赖管理相关的内容,在实际开发中,另一个重要的概念是测试,本文主要介绍 Go 测试相关的内容,包括单测规范、测试 Mock 以及基准测试。

重点内容

  • 单元测试
  • Mock 测试
  • 基准测试

知识点介绍

测试关系着系统的质量,质量则决定线上系统的稳定性。只要做好完备的测试,就可以避免事故的发生。

测试一般分为:

  • 回归测试:QA 同学手动通过终端回归一些固定的主流程场景
  • 集成测试:对系统功能维度做测试验证
  • 单元测试:开发者对单独的函数、模块做功能验证

单元测试

规则:

  • 所有测试文件以 test.go 结尾
  • func TestXxx( *testing.T)
  • 初始化逻辑放到 TestMain 中

举一个例子:

func HelloTom() string {
	return "Tom"
}
func TestHelloTom(t *testing.T) {
	output := HelloTom()
	expectOutput := "Tom"
	assert.Equal(t, expectOutput, output)
}

运行结果

ok  	github.com/Moonlight-Zhao/go-project-example/test	(cached)

如果 HelloTom 函数改成返回 “Jerry”,则结果为

--- FAIL: TestHelloTom (0.00s)
    e:\Every program\test_go\go-project-example-0\test\print_test.go:11: 
        	Error Trace:	print_test.go:11
        	Error:      	Not equal: 
        	            	expected: "Tom"
        	            	actual  : "Jerry"
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -1 +1 @@
        	            	-Tom
        	            	+Jerry
        	Test:       	TestHelloTom
FAIL

覆盖率:衡量代码是否经过了足够的测试,评价项目的测试水准,评估项目是否达到了高水准测试等级

一个示例,判断是否及格的函数

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

测试结果为:

ok      command-line-arguments  0.315s  coverage: 66.7% of statements

一共三行代码,只测试了两行,所以覆盖率为66.7%。如果改为如下代码

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

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

此时结果为:

ok      command-line-arguments  0.037s  coverage: 100.0% of statements

单元测试-依赖

实际功臣中复杂的项目,一般依赖较多,而单元测试需要保证稳定性和幂等性

  • 稳定性:相互隔离,能在任何时间、任何环境运行测试
  • 幂等性:每一次测试运行都应该产生与之前一样的结果

单元测试-Mock

使用 Monkey 库,Monkey 是一个开源的 mock 测试库,可以对方法或者实例进行 mock。

下面有一个示例,对 ReadFirstLine 函数打桩测试,不在依赖本地文件

func ReadFirstLine() string {
	open, err := os.Open("log")
	defer open.Close()
	if err != nil {
		return ""
	}
	scanner := bufio.NewScanner(open)
	for scanner.Scan() {
		return scanner.Text()
	}
	return ""
}
func ProcessFirstLine() string {
	line := ReadFirstLine()
	destLine := strings.ReplaceAll(line, "11", "00")
	return destLine
}
func TestProcessFirstLineWithMock(t *testing.T) {
	monkey.Patch(ReadFirstLine, func() string {
		return "line110"
	})
	defer monkey.Unpatch(ReadFirstLine)
	line := ProcessFirstLine()
	assert.Equal(t, "line000", line)
}

基准测试

基准测试是指测试一段程序的运行性能及耗费 CPU 的程度。在实际项目开发中经常会遇到代码性能瓶颈,需要利用基准测试对代码做性能分析来定位问题。

  • 优化代码,需要对当前代码进行分析
  • 内置的测试框架提供了基准测试的能力

下面通过一个例子来说明:随机选择执行服务器,有一个列表存放10个服务器,每次随机选择一个服务器

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 开头,输入参数为 *testing.B,Parallel 为多协程并发测试

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()
		}
	})
}

运行结果

BenchmarkSelect-16    	        85675731	        13.95 ns/op	       0 B/op	       0 allocs/op
BenchmarkSelectParallel-16    	26753168	        44.95 ns/op	       0 B/op	       0 allocs/op

可以看到代码在并发情况下存在劣化,主要原因是 rand 为了保证全局的随机性和并发安全,持有一把全局锁。为了解决这个问题,使用开源的高性能随机数方法 fastrand

func FastSelect() int {
	return ServerIndex[fastrand.Intn(10)]
}
func BenchmarkFastSelectParallel(b *testing.B) {
	InitServerIndex()
	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			FastSelect()
		}
	})
}

测试结果:

BenchmarkFastSelectParallel-16    	1000000000	         0.5244 ns/op	       0 B/op	       0 allocs/op

可以看到性能提升了百倍,但是需要注意:该函数牺牲了一定的数列一致性,适用大多数场景

总结

本文主要介绍了 Go 单元测试规范、测试 mock 以及基准测试,在实际项目开发中,测试有着不可或缺的作用,应熟练掌握测试的各种方法,避免事故的发生。