单元测试
以一个加法函数为例,对其进行单元测试。
首先编写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.Builder
,bytes.Buffer
相近,strings.Builder
更快 - 字符串在Go语言中是不可变类型,占用内存大小是固定的
- 使用
+
每次都会重新分配内存 strings.Builder
,bytes.Buffer
底层都是[]byte数组。内存扩容策略,不需要每次拼接重新分配内存- 预分配内存后,
strings.Builder
,bytes.Buffer
性能都有所提升