Go语言之测试 | 豆包MarsCode AI刷题

104 阅读6分钟

4. Go测试

4.1. 单元测试

在 Go 语言中,单元测试通常使用 Go 自带的 testing 包进行

4.1.1. 规则

  • 所有测试代码都以_test.go结尾。
  • 测试代码内部函数命名:func TestXxxx(t *testing.T),即必须以 Test 开头,并且接收一个指向 testing.T 类型的指针作为参数。
  • 初始化逻辑放到TestMain中
func TestMain(m *testing.M) {
    // 测试前:数据装载、配置初始化等前置工作

    code := m.Run()

    // 测试后:释放资源等收尾工作

    os.Exit(code)
}

4.1.2. 断言:assert

在单元测试中,assert 是用于验证代码输出是否符合预期的关键工具。它的作用是检查某个条件是否成立,如果条件不成立,则会抛出异常或显示错误消息,从而终止当前测试并标记为失败。

Go 语言的标准库中没有像其他语言(如 Python、Java)那样直接提供 assert 函数,但通过 testing 包中的 t.Errorft.Fatalf 来模拟断言的功能。此外,也有一些第三方库(如 Testify 提供了更丰富的断言功能。

标准库中的记录错误信息

Go 的 testing.T 提供了多种方法来执行断言,主要有:

  • t.Errorf:记录错误信息,但测试继续进行。
  • t.Fatalf:记录错误信息并终止测试。

第三方库中的断言

为了提高可读性和简化代码,很多 Go 项目使用第三方库,如Testify,它提供了丰富的断言方法,使得测试代码更加简洁和直观。

  • 安装 Testify

使用 go get 安装 Testify 库: go get github.com/stretchr/testify

  • 使用 Testify
package math

// 导入
import (
    "github.com/stretchr/testify/assert"
    "testing"
)

// TestSum 使用 Testify 的断言库
func TestSum(t *testing.T) {
    result := Sum(2, 3)
    expected := 5

    // 使用 assert.Equal 断言相等
    assert.Equal(t, expected, result, "Sum(2, 3) should be 5")
}

常见的 Testify 断言方法

  • assert.Equal(t, expected, actual, msg):断言 expectedactual 是否相等。
  • assert.NotEqual(t, expected, actual, msg):断言 expectedactual 是否不相等。
  • assert.Nil(t, obj, msg):断言对象 obj 是否为 nil
  • assert.NotNil(t, obj, msg):断言对象 obj 是否非 nil
  • assert.True(t, condition, msg):断言条件是否为 true
  • assert.False(t, condition, msg):断言条件是否为 false
  • assert.Error(t, err, msg):断言 err 是否为错误。
  • assert.NoError(t, err, msg):断言 err 是否没有错误。

4.1.3. 代码测试覆盖率

代码覆盖率是评估单元测试对代码的覆盖程度的一种指标。通过计算覆盖率,我们可以了解测试代码对项目源代码的覆盖情况,从而判断是否有未被测试的代码逻辑。

高覆盖率通常意味着更高的代码可靠性,但不一定意味着代码没有缺陷。

  1. 基本代码覆盖率命令
go test -cover

# 执行该命令后,会显示代码覆盖率的百分比,例如:
PASS
coverage: 85.0% of statements
ok      yourmodule/path    0.002s

2. 生成覆盖率详细报告

go test -coverprofile=coverage.out

这会在当前目录下生成一个 coverage.out 文件,包含每个语句的覆盖情况。可以使用 go tool cover 来查看详细的 HTML 报告:

go tool cover -html=coverage.out

3. 生成覆盖率详细报告

Go 的覆盖率工具支持以下几种标记选项,可以分别统计函数、语句等不同粒度的覆盖率:

    • -covermode=count:统计代码被覆盖的次数。
    • -covermode=set:仅记录是否覆盖,而不计数。
    • -covermode=atomic:类似于 count,但使用了原子计数操作,适合并发测试。

4.1.4. 简单测试示例

  • 第一步:编写代码和测试代码文件
package hellotom

func HelloTom() string {
    return "Hello Tom"
}
package hellotom

import "testing"

func TestHelloTom(t *testing.T) {
	output := HelloTom()
	expected := "Hello Tom"

	if output != expected {
		t.Errorf("Expected %s, but got %s", expected, output)   // 标准库模拟断言
	}
}
package hellotom

import (
	"github.com/stretchr/testify/assert"
	"testing"
)

func TestHello(t *testing.T) {
	output := HelloTom()
	expected := "Hello Tom"

	assert.Equal(t, output, expected)    // 第三方库断言
}
  • 第二步:运行测试

要运行测试,直接在所在目录下执行以下命令行命令:

go test

如果测试通过,输出将显示如下:

=== RUN   TestHelloTom
--- PASS: TestHelloTom (0.00s)
PASS
ok      github.com/Moonlight-Zhao/go-project-example/helloTom   (cached)

如果测试失败,t.Errorf 会输出错误信息,帮助你定位问题。

Go 也提供了测试覆盖率功能,来检查你的测试覆盖了代码中的多少部分, 还可以生成详细的覆盖率报告。

go test -cover

PASS
coverage: 100.0% of statements
ok      yourmodule/math    0.002s

4.2. MOCK测试

4.2.1. 基本概念

在单元测试中,Mock 测试(打桩测试) 用于模拟依赖组件的行为,以便在隔离的环境中测试代码的某一部分。Mock 对象可以替代真实的依赖组件,使测试更加灵活,并避免在测试中引入不可控的因素(如网络请求、数据库操作等)。

在 Go 中, 通常需要使用第三方库,比如 bou.ke/monkey 来实现 Monkey Patch。

4.2.2. monkey 使用示例

假设我们有一个函数 GetCurrentTime,它使用 time.Now() 返回当前时间。我们希望在测试中控制 time.Now() 的返回值,以便更精确地验证业务逻辑。

// timeutils.go
package timeutils

import (
    "time"
)

func GetCurrentTime() time.Time {
    return time.Now()
}

在实际测试时,直接调用 time.Now() 会产生动态数据,难以预测,因此我们可以使用 monkey 来替换 time.Now() 的实现。 如下所示:

package timeutils

import (
	"testing"
	"time"
	"github.com/bouk/monkey"
)

func TestGetCureentTime(t *testing.T) {
	// 设置固定的时间
	fixedtime := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)

	// 使用monkey代替time.Now()函数
	patch := monkey.patch(time.Now, func() time.Time {
		return fixedtime
	})
	defer patch.Unpatch() // 程序结束后,恢复time.Now()函数

	// 调用GetCureentTime()函数
	result := GetCurrentTime()

	// 验证是否是固定时间
	if result!= fixedtime {
		t.Errorf("Expected %v, but got %v", fixedtime, result)
	}

}

4.3. 基准测试

4.3.1. 基本概念

在 Go 中,基准测试(Benchmarking)是通过自带的 testing包 来进行性能评测的。基准测试的目的是测量函数的执行时间,从而了解代码在特定输入下的性能表现

Go 的基准测试通常会包含以下几个部分:

  1. 基准测试函数:函数的签名是 func BenchmarkXxx(b *testing.B),其中 Xxx 是你想要测试的函数名,b 是基准测试的对象。
  2. 测试逻辑:基准测试中通常会调用目标函数并重复执行,以测量它的平均执行时间。
  3. b.N:Go 在执行基准测试时会多次调用你的函数,每次执行的次数由 b.N 决定。b.N 是 Go 自动调整的,目的是让基准测试的时间足够长,以便得到准确的性能数据。

4.3.2. 测试示例

假设你有一个简单的函数,你想测试它的性能:

package mypackage

import (
	"fmt"
)

// 需要进行基准测试的函数
func Add(a, b int) int {
	return a + b
}

你可以为这个 Add 函数编写一个基准测试:

package mypackage

import (
	"testing"
)

// 基准测试函数
func BenchmarkAdd(b *testing.B) {
	// 在基准测试中,你应该避免每次都重新创建对象
	// 使用 b.N 来控制循环的次数
	for i := 0; i < b.N; i++ {
		Add(1, 2)
	}
}

运行基准测试:

在终端中,进入到包含测试文件的目录,执行以下命令:

go test -bench .    #   -bench . 命令会运行当前目录下所有以 Benchmark 开头的函数。
go test -bench BenchmarkAdd   # 只想运行特定的基准测试函数

基准测试输出:

$ go test -bench .
goos: linux
goarch: amd64
pkg: mypackage
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkAdd-12       1000000000     1.35 ns/op
PASS
ok      mypackage  1.428s
  • BenchmarkAdd-12:表示基准测试的函数名和 CPU 核心数(-12 表示 12 个 CPU 核心)。
  • 1000000000:表示运行的循环次数(即 b.N 的值)。
  • 1.35 ns/op:表示每次调用 Add 函数的平均耗时(单位:纳秒/次)。