Golang内置的测试框架的基本使用 | 青训营笔记

72 阅读4分钟

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

测试是系统开发中很重要的一环,关乎程序部署上线以后能否正常运行。测试分为回归测试、集成测试和单元测试,覆盖率依次变大,测试成本逐渐降低。

  • 回归测试主要针对整个系统的整体的所有功能进行测试。
  • 集成测试主要针对其中某一个功能进行测试。
  • 单元测试主要针对一个测试单元,可能是一个函数或一个模块进行测试。

因此,覆盖率最大的单元测试的质量在一定程度上决定了代码的质量,Golang 内置了测试框架用于进行单元测试。

单元测试

单元测试主要是对单独的一个函数或一个模块等测试单元进行测试,校对结果是否符合期望,从而判断测试单元是否存在错误。单元测试包含以下内容: Golang 单元测试的规则:

  • 所有测试文件以_test.go结尾。
  • 用于测试的函数以TestXxx(t *testing.T)命名。
  • 初始化逻辑放到TestMain中。

基本使用

  • 测试单元:一个函数

  • 测试文件:

  • 测试函数:

  • 使用go test命令运行:

  • 修改了错误,测试正确:

覆盖率

覆盖率是一项指标,用于衡量代码是否经过了足够的测试,评价项目的测试水准以及评估项目是否达到了高水准测试等级。 例如:以如下这个函数作为测试单元。

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

测试函数:

func TestJudgePassLineTrue(t *testing.T) {
    isPass := JudgePassLine(70)
    if !isPass {
        t.Errorf("Has error!")
    }
}

运行测试时加上--cover参数:

go test unittest/service_test.go unittest/service.go --cover

可以看到本次测试的覆盖率为75%。 因为测试函数执行以后,被测试函数只执行了score >= 60的分支,即只执行了 3 行代码,而整个被测试函数共有 4 行代码,所以覆盖率为 .

为提高覆盖率,增加一个测试函数:

func TestJudgePassLineFalse(t *testing.T) {
  isPass := JudgePassLine(50)
  if isPass {
    t.Errorf("Has error!")
  }
}

实际项目中:

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

Mock

实际项目中某个函数或模块的返回结果通常会依赖于文件、数据库、缓存等外部依赖,因此每次测试的结果可能会不一样。而单元测试要求具有幂等性和稳定性,即要求每次测试都返回同样的结果。因而在测试的时候,可以采用 Mock 的方式来构造数据,代替从数据库等外部依赖中获取数据,从而保证每次测试都返回相同结果,保证测试的幂等与稳定。

例如:

待测试函数依赖于文件log.txt

// 读取文件的第一行
func ReadFirstLine() string {
	open, err := os.Open("log.txt")
	defer open.Close() // 函数结束时调用,关闭资源
	if err != nil {
		return ""
	}
	scanner := bufio.NewScanner(open)
	for scanner.Scan() {
		return scanner.Text()
	}
	return ""
}

// 待测试函数
func UpperFirstLine() string {
	res := ReadFirstLine()
	return strings.ToUpper(res)
}

使用如下测试函数的话,一旦文件内容改变,测试结果也将改变:

func TestUpperFirstLine(t *testing.T) {
	res := UpperFirstLine()
	if res != "HELLO" {
		t.Errorf("res isn't HELLO but %v", res)
	}
}

因而可以采用 Mock 。

Mock 常用的第三方库:https://github.com/bouk/monkey

func TestUpperFirstLineWithMock(t *testing.T) {
	// 打桩,替换内存中的 ReadFirstLine 函数
	monkey.Patch(ReadFirstLine, func() string {
		return "hello"
	})
	// 结束后要换回来
	defer monkey.Unpatch(ReadFirstLine)
	res := UpperFirstLine()
	if res != "HELLO" {
		t.Errorf("res isn't HELLO but %v", res)
	}
}

这样即可保证测试时不依赖外部文件,从而保证测试的稳定性和幂等性。

基准测试

基准测试为测试一段程序运行时的性能与 CPU 的损耗。基准测试可进行代码分析,结果可用于优化代码。Golang 内置的测试框架也提供了基准测试的能力。

package unittest
​
import "math/rand"var serverIndex [10]intfunc InitServerIndex() {
  for i := 0; i < 10; i++ {
    serverIndex[i] = i + 100
  }
}
​
// 基准测试的被测试函数为 Select 函数
func SelectServer() int {
  return serverIndex[rand.Intn(10)]
}

测试函数:基准测试的测试函数以Benchmark开头。

package unittest
​
import "testing"func BenchmarkSelect(b *testing.B) {
  InitServerIndex()
  b.ResetTimer() // 下面才是真正开始测试 SelectServer 函数,所以要重置定时器时间
  for i := 0; i < b.N; i++ {
    SelectServer()
  }
}

测试结果: 结果分析:这里主要是因为rand.Intn函数为了保证全局的随机性和并发安全而在并行时持有全局锁,所以并行时性能会劣化。