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


    单元测试

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

    首先编写add.go文件:

    //add.go
    package main
    
    func add(a, b int) int {
    	return a + b
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

    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)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

    以下是运行结果:

    (base) PS F:\GolandProjects\GoProject1\main> go test
    Running short test
    PASS
    ok      GoProject1/main 0.489s
    
    • 1
    • 2
    • 3
    • 4

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

    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)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

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

    (base) PS F:\GolandProjects\GoProject1\main> go test -short
    Running short test
    Skipping long test
    PASS
    ok      GoProject1/main 0.448s
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

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

    (base) PS F:\GolandProjects\GoProject1\main> go test       
    Running short test
    Running long test
    PASS
    ok      GoProject1/main 0.417s
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    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)
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

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

    (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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

    基准测试

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

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

    //fib.go
    package main
    
    func Fib(n int) int {
        if n < 2 {
           return n
        }
        return Fib(n-1) + Fib(n-2)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    //fib_test.go
    package main
    
    import (
        "testing"
    )
    
    func BenchmarkFib10(b *testing.B) {
        for i := 0; i < b.N; i++ {
           Fib(10)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

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

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

    $ 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 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

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

    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()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    基准测试函数:

    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")
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    以下是运行结果:

    $ 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 使用拼接性能最差,strings.Builderbytes.Buffer相近,strings.Builder更快
    • 字符串在Go语言中是不可变类型,占用内存大小是固定的
    • 使用每次都会重新分配内存
    • strings.Builderbytes.Buffer底层都是[]byte数组。内存扩容策略,不需要每次拼接重新分配内存
    • 预分配内存后,strings.Builderbytes.Buffer性能都有所提升
  • 相关阅读:
    《深度探索C++对象模型》阅读笔记 第五章 构造、解构、拷贝语意学
    glTF格式详解
    复习 --- QT服务器客户端
    dvwa靶场通关(十二)
    科研DBCO-PEG-NIR-dyes|二苯并环辛炔-聚乙二醇-近红外染料|DBCO-PEG-近红外染料
    Netron 可视化Pytorh模型架构
    执行npm的时候报权限问题的解决方案
    ElasticSearch ES 安装 常见错误 Kibana安装 设置 权限 密码
    [SSM]MyBatisPlus高级
    系统异常SVC与PendSV指令
  • 原文地址:https://blog.csdn.net/m0_63230155/article/details/133019466