Go语言之测试 | 青训营

139 阅读4分钟

单元测试

单元测试规则

  1. 测试文件的名称应以_test.go结尾。例如,example_test.go是一个测试文件。
  2. 测试函数的名称应该以Test开头。例如,TestFunctionName是一个测试函数的命名规范。
  3. 测试函数应该有一个名为*testing.T的参数,用于处理测试失败和日志输出等情况。
  4. 在测试文件中,应该导入被测试代码所在的包和testing包。
  5. 可以使用go test -cover命令来查看测试覆盖率,即被测试代码的执行情况。
  6. 初始化逻辑放到TsetMain函数中。

代码示例

package main

func HelloTom() string{
    return "Jerry"
}
package main

import "testing"

func TestHelloTom(t *testing.T){
    output := HelloTom()
    expectOutput :="Tom"
    if output ≠ expectOutput{
        t.Errorf( "Ecpected %s do not match actual %s", expectOutput,output)
    }
}

运行命令go test

单元测试-assert

导包

import (
    "github.com/stretchr/testify/assert"
)
  1. t.Fail():表示测试失败,会打印错误信息并标记测试为失败。
  2. t.FailNow():表示立即停止当前的测试函数执行,并标记测试为失败。
  3. t.Errorf(format, args...):打印格式化的错误信息,并标记测试为失败。
  4. t.Fatalf(format, args...):打印格式化的严重错误信息,并立即停止当前的测试函数执行。
func TestHelloTom(t *testing.T){
    output := HelloTom()
    expectOutput :="Tom"
    assert.Equal(t, expectOutput, output)
}

运行结果

image.png

单元测试-覆盖率

用于评估单元测试,表示为对代码测试用例的覆盖度

go test --cover命令得到覆盖度

image.png

一般覆盖率50%-60%,80%+为较高覆盖率

测试分支要相互独立,全部覆盖

测试单元粒度要足够小,保证函数单一职责

Mock.

Mock测试是一种使用模拟对象(Mock Objects)来代替真实的对象来进行测试的测试方法。模拟对象根据预先定义的行为和期望来模拟真实对象的行为。通过使用模拟对象,可以在测试中模拟复杂的场景。Mock测试通常用于单元测试,其目的是隔离被测试对象并对其进行精确的测试。

打桩(stub)是一种在测试中用固定值或特定行为替换某个对象的方法。打桩可以控制对象在测试过程中的行为和返回值,以便更好地控制测试场景。通常,当我们测试一个对象时,可能会有一些外部依赖项,如数据库、网络服务等,打桩允许我们模拟这些依赖项的行为或返回固定的值,以便更容易编写和管理测试用例。有明确参数和返回值是最简单的打桩方式。

monkey是常用的打桩工具,使用时要注意

-   monkey不支持内联函数,测试时要通过命令行参数`-gcflags=-1`关闭go语言的内联优化
-   monkey不是线程安全的,因此最好不要用于并发的单元测试中

安装monkey:go get bou.ke/monkey

// 假设有一个包含用户服务的接口和实现

// UserService 是用户服务接口
type UserService interface {
	GetByID(id int) (*User, error)
	Save(user *User) error
}

// User 是一个用户结构体
type User struct {
	ID   int
	Name string
}

// UserServiceImpl 是 UserService 的具体实现
type UserServiceImpl struct {
	// ...
}

func (s *UserServiceImpl) GetByID(id int) (*User, error) {
	// 从数据库或其他资源中获取用户
	// ...
	return &User{ID: id, Name: "John Doe"}, nil
}

func (s *UserServiceImpl) Save(user *User) error {
	// 将用户保存到数据库或其他资源中
	// ...
	return nil
}
import (
	"testing"

	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"
)

func TestUserService_GetByID(t *testing.T) {
	// 创建 mock controller
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	// 创建模拟对象
	mockUserService := NewMockUserService(ctrl)

	// 设置打桩行为
	expectedUser := &User{ID: 1, Name: "Mock User"}
	mockUserService.EXPECT().GetByID(1).Return(expectedUser, nil)

	// 创建被测试的 UserServiceImpl,并注入模拟对象
	userService := &UserServiceImpl{
		UserService: mockUserService,
	}

	// 调用被测方法
	user, err := userService.GetByID(1)

	// 验证结果
	assert.NoError(t, err)
	assert.Equal(t, expectedUser, user)
}

func TestUserService_Save(t *testing.T) {
	// 创建 mock controller
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	// 创建模拟对象
	mockUserService := NewMockUserService(ctrl)

	// 设置打桩行为
	mockUserService.EXPECT().Save(gomock.Any()).Return(nil)

	// 创建被测试的 UserServiceImpl,并注入模拟对象
	userService := &UserServiceImpl{
		UserService: mockUserService,
	}

	// 调用被测方法
	err := userService.Save(&User{ID: 1, Name: "Test User"})

	// 验证结果
	assert.NoError(t, err)
}

monkey打桩示例

image.png

基准测试

基准测试是测量一个程序在固定工作负载下的性能。在Go语言中,基准测试函数和普通测试函数写法类似,但是以Benchmark为前缀名,并且带有一个*testing.B类型的参数;*testing.B参数除了提供和*testing.T类似的方法,还有额外一些和性能测量相关的方法。它还提供了一个整数N,用于指定操作执行的循环次数。

下面是IsPalindrome函数的基准测试,循环将执行N次

import "testing"

func BenchmarkIsPalindrome(b *testing.B) {
    for i := 0; i < b.N; i++ {
        IsPalindrome("A man, a plan, a canal: Panama")
    }
}