go语言测试入门[青训营]

136 阅读5分钟

本文是青训营课程- 《Go语言工程实践之测试》的笔记

image.png 回归测试:负责QA(质量保证)的同学手动用APP测试 集成测试:自动化的测试功能 单元测试:测试开发阶段,在一定程度上决定着代码的质量

1 单测(单元测试)

image.png

1.1 规则

  • 所有测试文件都以_test.go结尾

image.png

  • 测试函数命名规范:func TestXxx(*testing.T)

image.png

  • 初始化逻辑放到TestMain

image.png 例子:

func TestHelloTom(t *testing.T) {
	output := HelloTom()
	expectedOutput := "Tom"
	if output != expectedOutput {
		t.Errorf("Test failed, expected: '%s', got:  '%s'", expectedOutput, output)
	}
}

除了用!=判断输出与预期是否相符以外,还可以使用testifyassert

import (
	"github.com/stretchr/testify/assert"
	"testing"
)

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

这个用起来好像更爽一点。 image.png

1.2 单测的覆盖率

如何衡量代码是否经过了足够的测试? 如何评价项目的测试水准? 如何评估项目是否达到了高水准测试等级? 一个指标就能回答三个问题:代码覆盖率。 来个例子, 实现源代码文件如下:

func JudgePassLine(score int16) bool {
	if score >= 60 {
		return true
	} else {
		return false
	}
}

测试函数如下:

import (
	"github.com/stretchr/testify/assert"
	"testing"
)

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

使用go test执行测试代码时,加上--cover选项可以看到测试覆盖率的输出。

go test .\main_test.go .\main.go --cover
ok      command-line-arguments  0.249s  coverage: 66.7% of statements

为什么是66.7%呢?因为源代码中score<60的分支没有被测试。 如果把这部分也考虑到,那就会实现100%的单测覆盖率。 但是在实际项目中,100%的覆盖率是可望不可即的,一般的覆盖率在,超过的就属于比较高的(设计支付、资金的部分一般要求更高) 单测设计要求:测试分支相互独立、全面覆盖。应保证单元粒度足够小,函数单一职责。

2 Mock测试

2.3 依赖

项目的一些强依赖:文件、数据库、cache等。 单测的两个目标:

  • 幂等:重复运行一个测试程序,结果都是一样的。
  • 稳定:单测能够相互隔离,也就是说,能在任何时间独立运行。

如果直接写单测,那是不稳定的,因为访问这些依赖可能会有网络等其他原因的问题,我们在单测中一般用mock机制。 image.png 我们原函数实现读取文件中的第一行,然后将其中的"11"换成"00"输出,那现在我们的外部文件为上图中右下角所示,在这种情况下,我们当然知道输出应该为"line00",但是当所依赖的文件发生变化之后,这个测试函数就失效了.

2.4 Mock(打桩)

常用的mock的包:bou.ke/monkey 可以实现对函数和实例的方法进行mock, 快速Mock函数:

  • 为一个func打桩

我刚开始以为打桩是mock的一个释义呢,结果查了没查到,老师解释:打桩可以理解为用一个函数去替代,被称为原函数,就叫做打桩函数.

func TestProcessFirstLine(t *testing.T) {
	monkey.Patch(ReadFirstLine, func() string {
		return "line110"
	})
	defer monkey.Unpatch(ReadFirstLine)
	line := ProcessFirstLine()
	assert.Equal(t, "line000", line)
}

多说一嘴,我这里尝试导入"github.com/bouk/monkey"没有成功,所以这里我没有实现,但是可以先理解思想,这利用monkey.Patch()有两个参数,是两个函数,一个是被替换的ReadFirstLine,第二个是匿名函数,通过Patch实现临时替换(还是不理解为啥叫打桩……这个打桩有啥关系呢),defer测试完之后再替换回来. 这样,我们就解开了ProcessFirstLine对外部文件的依赖,从而实现测试.

  • 为一个mothod打桩

这里没有给例子,但是道理是一样的.

3 基准测试

go还提供了基准测试框架 基准测试:测试运行一段程序的性能和CPU的使用情况 我们在实际项目开发中,经常会遇到一些热点代码,或代码性能瓶颈问题,为了定位问题经常会对代码作性能分析(跑分),这就用到了基准测试.使用方法和单测类似. 举例,主程序如下

var ServerIndex [10]int

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

func Select() int {
	return ServerIndex[rand.Intn(10)]
}

func main() {
	Select()
}

针对Select()函数的测试程序设计如下

package main

import "testing"

func BenchmarkSelect(b *testing.B) {
	// Select要调用的ServerIndex()函数需要ServerIndex这歌全局变量资源
    // 初始化这个资源的过程应该计入跑分计时器
    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()
		}
	})
}

image.png 可以看到,并行的测试结果比串行要差(这被称为劣化),因为rand为了保证全局的随机性和并发安全,持有一个全局锁,这就降低了并发性能,在多协程并行压力测试的时候,性能就会劣化. 字节跳动提供了一个FastRand来解决这个问题,导入"github.com/bytedance/gopkg/lang/fastrand"后再实现一遍 image.png 对比效率,只能说牛逼.并发的压力测试效率提升了两百倍不止.但是FastRand的实现牺牲了一定的随机数列的一致性. 但是大多数场景下,fastrand都比rand好用.