go项目测试流程 | 青训营笔记

123 阅读4分钟

测试

测试分为:回归测试、集成测试、单元测试

byteDance3-1.png

单元测试

byteDance3-2.png

单元测试方便调试,避免每次出现bug时都直接重新编译部署,代价太大。

  • 所有测试文件以_test.go结尾
  • func TestXxx(*testing.T)
  • 初始化逻辑放到TestMain中
func TestPublishPost(t *testing.T) {
    
}

func TestMain(m *testing.M) {
    // 测试前:数据装载、配置初始化等前置工作
    code := m.Run() // 运行该package下的所有单元测试
    // 测试后:释放资源等收尾工作
    os.Exit(code)
}

覆盖率

  • 衡量代码是否经过了足够的测试

  • 评价项目的测试水准

  • 评估项目是否达到了高水准测试等级

package test

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

func TestJudgePassLineTrue(t *testing.T) {
	isPass := JudgePassLine(70)
	assert.Equal(t, true, isPass)
}

func TestJudgePassLineFail(t *testing.T) {
	isPass := JudgePassLine(50)
	assert.Equal(t, false, isPass)
}

Tips:

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

依赖

byteDance3-3.png

幂等性:单元测试执行多少次结果都一样

稳定性:单元测试相互隔离、能在任何时间、任何函数进行独立运行

如果直接在File、DB、Cache上运行单元测试则缺少稳定性,下面是一个文件处理的单元测试

// readFileFirstLine.go
func ReadFirstLine() string {
	open, err := os.Open("log")
	defer open.Close()
	if err != nil {
		return ""
	}
	scanner := bufio.NewScanner(open)
	for scanner.Scan() {
		return scanner.Text()
	}
	return ""
}

func ProcessFirstLine() string {
	line := ReadFirstLine()
	destLine := strings.ReplaceAll(line, "11", "00")
	return destLine
}
// TestreadFileFirstLine.go
func TestProcessFirstLine(t *testing.T) {
	firstLine := ProcessFirstLine()
	assert.Equal(t, "line00", firstLine)
}

这里的单元测试依赖于log文件,如果log文件被删除或者篡改那么单元测试的结果也会随着改变,不具有稳定性。所以我们采取具有稳定性的Mock测试

Mock测试

Mock测试是一种软件测试方法,用于测试程序中独立的模块或组件。Mock测试的目的是模拟测试组件的行为,以测试程序的其他部分是否能正确地与它交互。在Mock测试中,我们可以通过使用模拟对象来模拟一个组件或模块的行为,而不必实际运行该组件或模块。这种方法可以使测试更加快读、高效,也可以更容易地调试和诊断问题。Mock测试通常用于单元测试中,但也可以用于其他类型的测试。

快速mock函数

  • 为一个函数打桩
  • 为一个方法打桩

monkey: github.com/bouk/monkey 开源mock测试包,课程视频中采用的是这种,但介于该库已经很多年没有更新了,这里我采用的是这个包 github.com/stretchr/testify/mock。举一个简单的例子:

// readFileFirstLine.go
type LineReader interface {
	ReadFirstLine() string
}

func ReadFirstLine() string {
	open, err := os.Open("log")
	defer open.Close()
	if err != nil {
		return ""
	}
	scanner := bufio.NewScanner(open)
	for scanner.Scan() {
		return scanner.Text()
	}
	return ""
}

func ProcessFirstLine(lr LineReader) string {
	line := lr.ReadFirstLine()
	destLine := strings.ReplaceAll(line, "11", "00")
	return destLine
}
// readFileFirstLine_test.go
type MockLineReader struct {
	mock.Mock
}

func (mlr *MockLineReader) ReadFirstLine() string {
	args := mlr.Called()
	return args.String(0)
}

func TestProcessFirstLine(t *testing.T) {
	expectedLine := "110"
	mockReader := new(MockLineReader)
	mockReader.On("ReadFirstLine").Return(expectedLine)
	result := ProcessFirstLine(mockReader)
	expectedDestLine := "000"
	if result != expectedDestLine {
		t.Errorf("ProcessFirstLine() returned %v, expected %v", result, expectedDestLine)
	}
	mockReader.AssertExpectations(t)
}

基准测试

benchmark testing基准测试是一种测试方法,用于评估软件系统的性能、稳定性和可靠性。它通过在实际环境中执行各种任务和负载,来测试系统的性能,并且可以用来检测和排除性能问题和瓶颈。

在Go语言中,基准测试通过testing包的Benchmark函数来实现的。基准测试函数需要满足一定的命名规则,并且通过testing.B结构体来记录测试的结果,包括测试的时间、内存使用情况等等。基准测试可以用于测试各种代码片段的性能,例如算法实现、函数调用等等。

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

下面是一个基准测试的例子:

// benchTest.go
var ServerIndex [10]int

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

func Select() int {
	return ServerIndex[rand.Intn(10)]
	// rand里面有全局锁,会影响并发性能
}

func FastSelect() int {
	// fastrand牺牲了一定的随机数的一致性
	return ServerIndex[fastrand.Intn(10)]
}
// benchTest_test.go
func BenchmarkSelect(b *testing.B) {
	InitServerIndex()
	b.ResetTimer()
	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()
		}
	})
}

func BenchmarkSelectParallel1(b *testing.B) {
	InitServerIndex()
	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			FastSelect()
		}
	})
}

go test

  1. 进入项目根目录,执行go test,它会自动运行所有的测试文件,并输出测试结果。

  2. 通过命令行参数来控制go test命令的行为。例如:

    • -v 参数可以输出更详细的测试信息,包括每个测试用例的名称和运行时间等。
    • -run 参数可以指定运行哪些用例,可以使用通配符进行匹配。
    • -cover 参数可以生成测试覆盖率报告,用于评估测试用例的质量
  3. 在测试文件中,需要导入testing包,并编写测试函数。测试函数的名称必须以Test开头,并接受一个指向testing.T类型的参数,例如:

    func TestMyFunction(t *testing.T){
        // 测试代码
    }
    

​ 在测试函数中,可以使用t.Run()函数来执行子测试,也可以使用t.Helper()函数来标记测试函数是一个辅助函数,用于定位测试失败。

  1. 运行go test命令时,会自动搜索项目中所有以_test.go结尾的文件,并执行其中的测试函数。