Go语言工程化实践-测试 | 青训营

27 阅读6分钟

go语言工程实践-测试

测试是开发过程的重要部分,也是软件开发生命周期的关键部分。 它可以确保应用程序正常运行和满足客户需求。测试关系着系统的质量,质量则决定线上系统的稳定性,一旦出现bug漏洞,就会造成事故。

测试是避免事故的最后一道屏障,只要做好完备的测试,就可以避免事故的发生。

测试一般分为以下三种:

  • 回归测试:使用APP,如刷抖音看评论
  • 集成测试:对功能维度进行自动化测试
  • 单元测试:对单独的函数、模块做验证
    以上三个:从上到下,测试成本下降,覆盖率上升

image.png

下面主要从以下三部分介绍一下测试

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

单元测试

image.png

单元测试主要包括:输入、测试单元、输出、以及校对。

单元的概念比较广,包括接口,函数,模块等。用最后的校对来保证代码的功能与期望相符。

单元测试一方面可以保证质量,在整体覆盖率足够的情况下,一定程度上既保证了新功能本身的正确性,又未破坏原有代码的正确性。另一方面可以提升效率,在代码有bug的情况下,通过编写单元测试,可以在一个短周期内定位和修复问题。

单元测试-规则

  • 命名:所有的测试文件以_test.go结尾
  • 函数名:func TestXxx(*testing.T) Test开头,且连接的第一个字母大写
  • 初始化逻辑放到TestMain中:
func TestMain(m *testing.M){
	//测试前:数据装载、 配置初始化等
	code:=m.Run()
	//测试后:释放资源等
	os.Exit(code7777776)
}

单元测试-例子1

func HelloTom() string {
	return "Jerry"
}
func TestHelloTom(t *testing.T) {
	output := HelloTom()
	expectOutput := "Tom"
	if output != expectOutput {
		t.Errorf("Expected %s do not match actual %s", expectOutput, output)
	}
}

此时,我们的期望和函数输出不符,运行结果如下:

image.png 可以看到我们的函数出了问题,那么就要修复这个函数。

单元测试-assert 修复函数

import (
	"testing"
	"github.com/stretchr/testify/assert"
)
func HelloTom() string {
	return "Tom"
}
func TestHelloTom(t *testing.T) {
	output := HelloTom()
	expectOutput := "Tom"
	assert.Equal(t, expectOutput, output)
}
func main() {
	testing.Main(func(pat, str string) (bool, error) { return true, nil }, []testing.InternalTest{{"TestHelloTom", TestHelloTom}}, []testing.InternalBenchmark{}, []testing.InternalExample{})
}

运行成功,验证了HelloTom函数的正确。

上述即为单元测试的用法

单元测试-覆盖率

覆盖率就是用来评估单元测试

image.png

image.png 如图,我们传入score参数为70,在JudgePassLine函数内,if语句和内部语句执行,最后一行返回false没有执行,执行区域占2/3,故输出覆盖率为66.7%。

提升覆盖率

image.png 我们可以增加一个不及格的测试case,重新执行所有单元测试,最终覆盖率为100%。

在实际项目,覆盖率为100%是一个期望值,很难达到,覆盖率有以下特点:

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

依赖

工程项目,一般会依赖组件等一些强依赖,组件也很复杂。我们的单元测试需要保证稳定性和幂等性。

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

而要实现这一目标就要用到mock机制。

image.png

Mock测试

处理文件(如改变数据库)后,可能原本依赖的输入不正确了,程序也就不能运行了。为了避免这个问题,进行Mock打桩。

GoMock是一个Go框架,它与内置的测试包整合得很好,用于解决但原创而是中遇到的外部依赖问题,并且还有Mockgen工具用来辅助生成相关的mock代码。

首先我们需要先安装gomock和mockgen

# 安装gomock和mockgen
go get -u github.com/golang/mock/gomock
go install github.com/golang/mock/mockgen

快速Mock函数:

  • 为一个函数打桩
  • 为一个方法打桩

就可以理解为,用函数A代替函数B,B是源函数,A就是打桩函数。

Go语言中有很多第三方库提供了 mock 功能,例如Monkey这个库,可以简单地利用Patch将函数的运行地址替换成原来的,最后Unpatch替换回来。保证单元测试的稳定性。

github.com/bouk/monkey 包里面包含两个方法:patch和unpatch

  • patch里面参数target表示原函数,replacement代表我们需要打桩的函数
  • unpatch表示在打桩后,把这个包卸载掉。

Mockey patch的作用域在runtime,在运行时通过Go的unsafe包,能够将内存中函数的地址替换为运行时函数的地址。

image.png

下面就来举例看一下Mock的使用:

import (
	"fmt"
	"github.com/bouk/monkey"
)

func main() {
	// 要替换的函数
	originalFunction := func() string {
		return "Hello, World!"
	}

	// 替换函数
	patch := monkey.Patch(originalFunction, func() string {
		return "Goodbye, World!"
	})
	defer patch.Unpatch()

	fmt.Println(originalFunction())
	// 输出: Goodbye, World!
}

在这个例子中,我们使用了 monkey 库中的 Patch 函数来替换 originalFunction 函数,并在最后使用 Unpatch 函数来撤销替换。在这个例子中,替换后的函数将总是返回 “Goodbye, World!”。

基准测试

基准测试是指测试一段程序的运行性能及耗费cpu的程度。 而我们在实际项目开发中会经常遇到代码性能瓶颈,为了定位问题经常要对代码做性能分析,这就用到了基准测试。 使用方法类似于单元函数,函数以Benchmark开头。

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

如服务器负载均衡中使用到了rand函数,测试时使用串行测试和并行测试效率不同。fastrand函数代替rand函数,发现性能有巨大提升。

//源文件
func Add(a, b int) int {
	return a + b
}
//测试文件
import (
	"testing"
)

func BenchmarkAdd(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Add(2, 3)
	}
}

测试:在当前目录go文件的目录终端下输入go test -bench=BenchmarkAdd BenchmarkAdd是测试函数的名称。运行结果如下:

image.png

每个基准函数被执行时都有一个不同的b.N值,这个值代表基准函数应该执行的迭代次数。

b.N从1开始,如果基准函数在1秒内就执行完了,那么b.N的值会递增以便基准函数再重新执行(即基准函数默认要运行1秒,如果该函数的执行时间在1秒内就运行完了,那么就递增b.N的值,重新再执行一次)

b.N按照近似顺序增加,每次迭代大约增长20%。基准框架试图更智能,如果它看到较小的b.N值相对较快的完成了迭代,它将b.N增加的更快。

在上面的BenchmarkAdd的例子中,我们发现迭代大约1000000000 次耗时0.748秒,平均每次运行耗时0.1195纳秒。

通过基准测试的结果,我们可以评估代码的性能。我们可以使用不同的输入参数、不同的算法或优化技术来改进代码的性能,并通过基准测试来验证优化效果。