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性能都有所提升
相关推荐
道之极万物灭22 分钟前
Go小工具合集
开发语言·后端·golang
瑞士卷@22 分钟前
MyBatis入门到精通(Mybatis学习笔记)
java·数据库·后端·mybatis
yuuki2332331 小时前
【C语言】文件操作(附源码与图片)
c语言·后端
IT_陈寒1 小时前
Python+AI实战:用LangChain构建智能问答系统的5个核心技巧
前端·人工智能·后端
无名之辈J1 小时前
系统崩溃(OOM)
后端
码农刚子1 小时前
ASP.NET Core Blazor简介和快速入门 二(组件基础)
javascript·后端
间彧1 小时前
Java ConcurrentHashMap如何合理指定初始容量
后端
catchadmin2 小时前
PHP8.5 的新 URI 扩展
开发语言·后端·php
少妇的美梦2 小时前
Maven Profile 教程
后端·maven
白衣鸽子2 小时前
RPO 与 RTO:分布式系统容灾的双子星
后端·架构