Go 语言工程实践之测试

Go 语言工程实践之

测试是避免事故的最后一道屏障

  • 回归测试:在软件经过修改(如修复错误、添加新功能等)后,重新对软件开展的测试,目的在于确保修改未引入新错误且已通过测试的功能依旧正常运行。
  • 集成测试:在单元测试基础上,将已通过单元测试的各单元依软件设计架构组合起来进行测试,重点关注单元间接口的正确性以及协同工作时整体功能的实现情况。
  • 单元测试:对软件中最小可测试单元(如函数、方法、类等)进行的测试,着重验证其内部逻辑正确性,确保各单元按预期单独运行。

从上到下,覆盖率逐层变大,成本却逐层降低

一、单元测试

1. 规则

  • 所有测试文件以 _test.go 结尾
  • func TestXxx(*testing.T)
  • 初始化逻辑放到 TestMain
1
2
3
4
5
6
7
8
9
10
func TestPublishPost(t *testing.T) {
}

func TestMain(m *testing.M) {
// 测试前:数据装载、配置初始化等前置工作
code := m.Run()
// 测试后:释放资源等首尾工作
os.Exit(code)
}

2. 例子

image-20241115174721563

运行

image-20241115174522778

3. assert

github.com/stretchr/testify/assert 是 Go 语言中一个非常受欢迎的用于编写测试用例时进行断言操作的库。

在编写测试用例时,我们常常需要验证被测试函数或代码片段的输出是否符合预期。assert 库提供了一系列方便的断言函数,让开发者能够简洁明了地表达这些预期,并在实际结果与预期不符时,清晰地报告出错误信息,从而帮助快速定位问题所在。

优点

  • 简洁清晰:相比于 Go 语言自带的测试框架中较为基础的断言方式,assert 库提供的断言函数使用起来更加简洁明了,能够快速准确地表达测试意图。
  • 丰富的错误信息:当断言失败时,assert 库会输出详细的错误信息,包括预期值、实际值等,这有助于开发者迅速定位问题所在,提高测试效率。

4. 覆盖率

  • 语句覆盖率:最基本的一种覆盖率衡量方式,它表示测试用例执行过程中,被执行到的语句数量占总语句数量的百分比。例如,一个函数中有 10 条语句,测试用例执行后有 8 条语句被执行到了,那么语句覆盖率就是 80%。
  • 分支覆盖率:关注的是程序中的条件分支语句,如 ifelseswitch 等。它衡量的是测试用例执行过程中,这些条件分支语句的不同分支被执行到的情况占所有可能分支的百分比。
  • 条件覆盖率:主要针对条件表达式中的各个条件因子。它考察的是在条件表达式(如 if (a && b) 中的 ab 等条件因子)中,每个条件因子的不同取值组合被测试用例覆盖到的情况占所有可能取值组合的百分比

样例

1
2
3
4
5
6
7
8
9
10
// judgment.god
package main

func JudgePassLine(score int16) bool {
if score >= 60 {
return true
}
return false
}

覆盖率的重要性与局限性

  • 帮助发现未测试到的代码区域:通过查看覆盖率报告,能够直观地了解到哪些部分的代码在测试过程中没有被执行到,从而有针对性地补充测试用例,提高测试的全面性。
  • 不能完全代表测试质量:即使覆盖率达到了很高的水平,比如 100% 的语句覆盖率,也不能保证测试用例已经完美地测试了所有可能的情况。因为可能存在虽然语句被执行了,但执行的逻辑顺序、输入值的组合等方面仍然存在问题的情况。

Tips

  • 一般覆盖率:50%~60%,较高覆盖率80%+,
  • 测试分支相互独立、全面覆盖。
  • 测试单元粒度足够小,函数单一职责

二、Mock测试

1. 依赖

如果被测试的代码依赖于外部资源,如数据库、网络服务等,在测试时通常不希望直接连接到真实的外部资源,因为这可能导致测试不稳定(外部资源状态变化、网络问题等)且测试速度慢。

测试稳定性

测试环境的稳定性:确保测试环境的一致性对于测试的稳定性至关重要。这包括 Go 版本的一致、依赖库版本的一致等。使用版本控制工具(如 Go Modules)来精确管理项目的依赖库版本,避免因为依赖库的意外更新导致测试失败。

被测试代码的稳定性:被测试代码本身应该具有良好的设计和实现,遵循可靠的编程原则,如避免全局变量的滥用(因为全局变量可能在不同测试用例之间产生意外的影响),合理处理并发操作(使用正确的锁机制等保证并发安全)。

幂等

幂等性是指对同一操作的多次重复执行应该产生与一次执行相同的效果。例如,一个函数用于向数据库中插入一条特定记录,如果该函数是幂等的,那么无论调用它一次还是多次,数据库中最终都只会存在一条符合条件的记录。

2. Mock

  • 在测试过程中,经常需要隔离被测试单元与它的依赖项,以便能够独立地测试被测试单元的功能,而不受依赖项实际行为的影响。Mock 机制就是用于创建模拟的依赖对象,这些模拟对象可以按照测试的需要返回特定的值、模拟特定的行为或记录被调用的情况等。

  • 例如一个开源库 monkey : https://github.com/bouk/monkey

  • 用于将一个函数(target)替换为另一个函数(replacement)。

    1
    2
    3
    4
    5
    6
    7
    8
    // Patch replaces a function with another
    func Patch(target, replacement interface{}) *PatchGuard {
    t := reflect.ValueOf(target)
    r := reflect.ValueOf(replacement)
    patchValue(t, r)

    return &PatchGuard{t, r}
    }
  • 用于移除对目标函数的任何猴子补丁,即将函数恢复到原始状态

    1
    2
    3
    4
    5
    // Unpatch removes any monkey patches on target
    // returns whether target was patched in the first place
    func Unpatch(target interface{}) bool {
    return unpatchValue(reflect.ValueOf(target))
    }

2. 样例

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
// MockTest.go
package main

import (
"bufio"
"os"
"strings"
)
// 读取文件外部依赖
func ReadFirstLine() string {
open, err := os.Open("../test/xx.txt")
defer open.Close()
if err != nil {
return ""
}
scanner := bufio.NewScanner(open)
for scanner.Scan() {
return scanner.Text()
}
return ""
}
// 要测试的函数
func ProcessFirstLne() string {
line := ReadFirstLine()
destLine := strings.ReplaceAll(line, "11", "00")
return destLine
}
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
// 测试代码 Mock_test.go
package main

import (
// 导入monkey库,用于实现函数的猴子补丁(在运行时替换函数的行为)
"bou.ke/monkey"
"github.com/stretchr/testify/assert"
"testing"
)

func TestProcessFirstLineWithMock(t *testing.T) {
// 使用monkey.Patch函数来替换ReadFirstLine函数的实现
monkey.Patch(ReadFirstLine, func() string {
return "line110"
})
// 为了恢复ReadFirstLine函数的原始实现,避免对其他测试用例产生影响
defer monkey.Unpatch(ReadFirstLine)

// 调用ProcessFirstLne函数,此时由于之前的Patch操作,它将调用被替换后的ReadFirstLine函数
line := ProcessFirstLne()

// 使用assert.Equal来断言ProcessFirstLne函数的返回值是否等于预期的"line000"
// 如果不相等,测试将失败,并输出详细的错误信息
assert.Equal(t, "line000", line)
}

image-20241117143436491

二、基准测试

基准测试是一种性能测试手段,用于评估代码在特定条件下的执行效率和性能表现

  • 优化代码,需要对当前代码分析
  • 内置的测试框架提供了基准测试的能力

1. 例子

服务器负载均衡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import "math/rand"

// 初始化10个服务器
var ServerIndex [10]int

func InitServerIndex() {
for i := 0; i < 10; i++ {
ServerIndex[i] = i + 100
}
}

// 随机选一个服务器执行,对此函数做一个基准测试
func Select() int {
return ServerIndex[rand.Intn(10)]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import "testing"

func BenchmarkSelect(b *testing.B) {
InitServerIndex() // 不属于测试
b.ResetTimer() // 所以在这里进行时间重置

// for串行的压力(基准)测试
for i := 0; i < b.N; i++ {
Select()
}
}

func BenchmarkSelectParallel(b *testing.B) {
InitServerIndex()
b.ResetTimer()
// 并行测试
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Select()
}
})
}

image-20241117150710877


Go 语言工程实践之测试
https://leaf-domain.gitee.io/2025/03/22/go/Go 语言工程实践之测试/
作者
叶域
发布于
2025年3月22日
许可协议