Go语言的单元测试与基准测试详解

单元测试

以一个加法函数为例,对其进行单元测试。

首先编写add.go文件:

go 复制代码
//add.go
package main

func add(a, b int) int {
	return a + b
}

其次编写add_test.go文件,在go语言中,测试文件均已_test结尾,这里只需要在被测试的文件后加上_test即可。并且测试文件与要被测试的文件需要放在同一个包中,并不像Java那样需要将所有的测试文件放在一个专门的测试文件夹里面,例如我将这两个文件都放在main包下:

go 复制代码
package main

import (
	"fmt"
	"testing"
)

//测试函数需要以Test开头
func TestAdd(t *testing.T) {
	fmt.Println("Running short test")
	res := add(1, 2)
	if res != 3 {
		t.Errorf("add(1,2) should be 3, got %d", res)
	}
}

cd到测试文件的目录,执行测试命令go test

以下是运行结果:

bash 复制代码
(base) PS F:\GolandProjects\GoProject1\main> go test
Running short test
PASS
ok      GoProject1/main 0.489s

如果想在测试中跳过那些需要耗时比较长的测试,可以做以下处理:

go 复制代码
package main

import (
	"fmt"
	"testing"
)

func TestAdd(t *testing.T) {
	fmt.Println("Running short test")
	res := add(1, 2)
	if res != 3 {
		t.Errorf("add(1,2) should be 3, got %d", res)
	}
}

func TestAdd2(t *testing.T) {
	if testing.Short() {
		fmt.Println("Skipping long test")
        //短测试模式就跳过该测试
		t.Skip("Skipping long test")
	}
	fmt.Println("Running long test")
	res := add(5, 6)
	if res != 11 {
		t.Errorf("add(5,6) should be 11, got %d", res)
	}
}

在运行时指执行短测试,只需要执行go test -short

bash 复制代码
(base) PS F:\GolandProjects\GoProject1\main> go test -short
Running short test
Skipping long test
PASS
ok      GoProject1/main 0.448s

我们发现跳过了第二个测试,也就是测试函数TestAdd2

当然如果还是执行go test命令,则两个测试都将会运行:

bash 复制代码
(base) PS F:\GolandProjects\GoProject1\main> go test       
Running short test
Running long test
PASS
ok      GoProject1/main 0.417s

如果想要同时测试很多条数据,可以按如下的方式处理,而不需要写很多的函数:

go 复制代码
func TestAdd3(t *testing.T) {
	var dataset = []struct {
		a, b, expected int
	}{
		{1, 2, 3},
		{5, 6, 11},
		{10, 20, 30},
		{100, 200, 300},
	}
	for _, d := range dataset {
		res := add(d.a, d.b)
		if res != d.expected {
			t.Errorf("add(%d,%d) should be %d, got %d", d.a, d.b, d.expected, res)
		}
	}
}

这里我们用go test -v测试一下:

diff 复制代码
(base) PS F:\GolandProjects\GoProject1\main> go test -v
=== RUN   TestAdd
Running short test
--- PASS: TestAdd (0.00s)
=== RUN   TestAdd2
Running long test
--- PASS: TestAdd2 (0.00s)
=== RUN   TestAdd3
--- PASS: TestAdd3 (0.00s)
PASS
ok      GoProject1/main 0.408s

go test 用于运行测试并显示简洁的结果,而 go test -v 用于以详细模式运行测试并提供更多的输出信息,有助于更深入地了解测试的运行情况。通常,在开发和调试过程中,使用 -v 标志是很有帮助的,但在持续集成和自动化测试中,可能更倾向于使用简洁的 go test,以便更容易解释测试结果。

基准测试

性能表现需要实际数据衡量,Go语言提供了支持基准性能测试的benchmark工具。基准测试用于确定一段代码的执行速度和性能,并可以用来优化和改进代码。

以编写斐波那契函数为例:

go 复制代码
//fib.go
package main

func Fib(n int) int {
    if n < 2 {
       return n
    }
    return Fib(n-1) + Fib(n-2)
}
go 复制代码
//fib_test.go
package main

import (
    "testing"
)

func BenchmarkFib10(b *testing.B) {
    for i := 0; i < b.N; i++ {
       Fib(10)
    }
}

benchmark 和普通的单元测试用例一样,都位于 _test.go 文件中。 函数名以 Benchmark 开头,参数是 b *testing.B。和普通的单元测试用例很像,单元测试函数名以 Test 开头,参数是t *testing.T。使用 b.N 控制循环次数:b.N 是基准测试的循环次数,它会根据不同的运行情况自动调整,以保证结果的可比性。

  • 运行当前 package 内的用例:go test .
  • 运行子 package 内的用例: go test ./<package name>
  • 如果想递归测试当前目录下的所有的 package:go test ./...

go test 命令默认不运行 benchmark 用例的,如果我们想运行 benchmark 用例,需要加上 -bench 参数。例如:

bash 复制代码
$ go test -bench .
goos: windows
goarch: amd64
pkg: GoProject1
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
BenchmarkFib10-16        5496252               212.5 ns/op
PASS
ok      GoProject1      1.454s
  • goos: windows:这行显示运行基准测试的操作系统,此处为 Windows。
  • goarch: amd64:这行显示运行基准测试的机器架构,此处为 64 位 AMD 架构。
  • pkg: GoProject1:这行显示包含基准测试代码的包名,此处为 "GoProject1"。
  • cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz:这行显示运行基准测试的机器 CPU 信息,包括 CPU 型号和时钟频率。
  • PASS:这行表示所有的测试,包括基准测试,都已成功通过。
  • ok GoProject1 1.454s:这行显示所有测试,包括基准测试,的整体执行时间。在这种情况下,整个测试套件执行时间大约为 1.454 秒。
  • BenchmarkFib10-16 是测试函数名,-16表示GOMAXPROCS的值为16,GOMAXPROCS 1.5版本后,默认值为CPU核数 。5496252 表示一共执行5496252 次,即b.N的值。212.5 ns/op表示每次执行花费212.5ns

再举一个比较详细的例子,比较不同字符串处理方式的性能:

go 复制代码
func Plus(n int, str string) string {
    s := ""
    for i := 0; i < n; i++ {
       s += str
    }
    return s
}

func StrBuilder(n int, str string) string {
    var builder strings.Builder
    for i := 0; i < n; i++ {
       builder.WriteString(str)
    }
    return builder.String()
}

func ByteBuffer(n int, str string) string {
    buf := new(bytes.Buffer)
    for i := 0; i < n; i++ {
       buf.WriteString(str)
    }
    return buf.String()
}

func PreStrBuilder(n int, str string) string {
	var builder strings.Builder
	builder.Grow(n * len(str))
	for i := 0; i < n; i++ {
		builder.WriteString(str)
	}
	return builder.String()
}
func PreStrByteBuffer(n int, str string) string {
	buf := new(bytes.Buffer)
	buf.Grow(n * len(str))
	for i := 0; i < n; i++ {
		buf.WriteString(str)
	}
	return buf.String()
}

基准测试函数:

go 复制代码
func BenchmarkPlus(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Plus(100000, "wxy")
	}
}

func BenchmarkStrBuilder(b *testing.B) {
	for i := 0; i < b.N; i++ {
		StrBuilder(100000, "wxy")
	}
}

func BenchmarkByteBuffer(b *testing.B) {
	for i := 0; i < b.N; i++ {
		ByteBuffer(100000, "wxy")
	}
}

func BenchmarkPreStrBuilder(b *testing.B) {
	for i := 0; i < b.N; i++ {
		PreStrBuilder(100000, "wxy")
	}
}

func BenchmarkPreByteBuffer(b *testing.B) {
	for i := 0; i < b.N; i++ {
		PreStrByteBuffer(100000, "wxy")
	}
}

以下是运行结果:

bash 复制代码
$ go test -bench .
goos: windows
goarch: amd64
pkg: GoProject1
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
BenchmarkPlus-16                       1        1126084200 ns/op
BenchmarkStrBuilder-16              3982            284773 ns/op
BenchmarkByteBuffer-16              2947            485091 ns/op
BenchmarkPreStrBuilder-16           4771            278961 ns/op
BenchmarkPreByteBuffer-16           3310            364676 ns/op
PASS
ok      GoProject1      6.457s
  • 使用拼接性能最差,strings.Builderbytes.Buffer相近,strings.Builder更快
  • 字符串在Go语言中是不可变类型,占用内存大小是固定的
  • 使用每次都会重新分配内存
  • strings.Builderbytes.Buffer底层都是[]byte数组。内存扩容策略,不需要每次拼接重新分配内存
  • 预分配内存后,strings.Builderbytes.Buffer性能都有所提升
相关推荐
仙俊红18 分钟前
Spring Cloud 核心组件部署方式速查表
后端·spring·spring cloud
码农幻想梦41 分钟前
实验九 Restful和ajax实现
后端·ajax·restful
今天多喝热水1 小时前
SpEL(Spring Expression Language) 表达式
java·后端·spring
码农水水1 小时前
浅谈 MySQL InnoDB 的内存组件
java·开发语言·数据库·后端·mysql·面试
独自破碎E1 小时前
Spring Boot的多环境配置
java·spring boot·后端
Edward-tan2 小时前
【玩转全栈】----Django模板语法、请求与响应
后端·python·django
猫头鹰源码(同名B站)2 小时前
基于django+vue的时尚穿搭社区(商城)(前后端分离)
前端·javascript·vue.js·后端·python·django
Watermelo6172 小时前
随机扣款实现赛博共产主义,《明日方舟:终末地》公测支付事故复盘
数据库·后端·游戏程序·技术美术·用户体验·游戏策划·游戏美术
观音山保我别报错2 小时前
Spring Boot 项目学习内容详解(一)
spring boot·后端·学习
哪里不会点哪里.2 小时前
Spring Boot 启动原理深度解析
java·spring boot·后端