菜单

Go单元测试

alexpang
alexpang
发布于 2023-10-19 / 204 阅读
0
1

Go单元测试

Go单元测试框架

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。

单元测试(模块测试)是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。通常而言,一个单元测试是用于判断某个特定条件(或者场景)下某个特定函数的行为。例如,你可能把一个很大的值放入一个有序list 中去,然后确认该值出现在list 的尾部。或者,你可能会从字符串中删除匹配某种模式的字符,然后确认字符串确实不再包含这些字符了。
单元测试是由程序员自己来完成,最终受益的也是程序员自己。可以这么说,程序员有责任编写功能代码,同时也就有责任为自己的代码编写单元测试。执行单元测试,就是为了证明这段代码的行为和我们期望的一致。

笔者采用单元测试框架为 Convey、Monkey、SqlMock。

1.GoMonkey

使用GoMonkey有如下好处:

  • 隔离被测代码

  • 加速执行测试

  • 使得执行变得确定

  • 模拟特殊情况

支持功能列表:

  • 支持为函数打桩

  • 支持为一个函数打一个特定的桩序列

  • 支持为成员方法打桩

  • 支持为函数变量打桩

  • 支持为一个接口打桩

GoMonkey对于变量的mock原理跟gostub一样通过reflect包实现。

打桩解释:将测试任务之外的,并且与测试任务相关的任务用桩代理,从而实现分离测试任务。例如funcA调用funcB,funcB调用funcC和funcD,如果用桩函数B1代替函数B那么,函数A就可以完全隔断与funcC、funcD的关系。

补齐解释:用桩函数代替为实现的代码。例如funcA调用funcB,而funcB还未实现,则可以代替funcB使得funcA可以使用。

控制解释:使用桩函数声明完全一样的funcB1,根据测试的需要输出合适的数据。

2.实现原理

gomonkey是为函数、变量方法打桩,但是对于函数方法的模拟替换,在Go这种静态类型中不太容易,因此需要再“运行时”替换,即是通过创建汇编指令实现。

核心是通过调用 ApplyCore方法。replace 通过替换内存地址来实现。

func (this *Patches) ApplyCore(target, double reflect.Value) *Patches {
	this.check(target, double)
	if _, ok := this.originals[target]; ok {
		panic("patch has been existed")
	}

	this.valueHolders[double] = double
	original := replace(*(*uintptr)(getPointer(target)), uintptr(getPointer(double)))
	this.originals[target] = original
	return this
}
func replace(target, double uintptr) []byte {
	code := buildJmpDirective(double)
	bytes := entryAddress(target, len(code))
	original := make([]byte, len(bytes))
	copy(original, bytes)
	modifyBinary(target, code)
	return original
}

3.Convey和GoMonkey基本使用

新建函数 ReadJsonFile。


func ReadJsonFile(filename string, m interface{}) error {
	f, err := os.Open(filename)
	if err != nil {
		return fmt.Errorf("open filename falied : %v", err.Error())
	}
	buf, err := io.ReadAll(f)
	if err != nil {
		return fmt.Errorf("read filename falied : %v", err.Error())
	}
	err = json.Unmarshal(buf, m)
	if err != nil {
		return fmt.Errorf("unmarshal failed : %v", err.Error())
	}
	return nil
}

编写测试用例

测试两种场景,第一种对于ReadJsonFile进行打桩,抛出错误,第二种测试正常输出


func TestReadJsonFile(t *testing.T) {
	convey.Convey("TestReadJsonFile", t, func() {
		convey.Convey("Should be return error when open file error", func() {
			patch := gomonkey.ApplyFunc(ReadJsonFile, func(filename string, target interface{}) error {
				return fmt.Errorf("open file error")
			})
			defer patch.Reset()
			var ret []string
			err := ReadJsonFile("abc", &ret)
			convey.So(err, convey.ShouldEqual, fmt.Errorf(`open file error`))
		})
		convey.Convey("Should be return nil when success", func() {
			var ret struct{ Msg string }
			err := ReadJsonFile("mock.json", &ret)
			convey.So(err, convey.ShouldEqual, nil)
		})
	})
}

执行测试用例:

go test -v --run TestReadJsonFile  -gcflags=all=-l

3.1 无返回值的函数示例

在Go开发中,很多时候我们都要传递指针类型的参数,然后再函数内部修改具体的值,而没有返回值。

比如

func UpdateUser(user *User) error {
	fmt.Println(user) // 第一次打印user
	err := UpdateInferUser(user)
	fmt.Println(user) //  第二次打印user
	return err
}

func UpdateInferUser(user *User) error {
	if user == nil {
		return fmt.Errorf("user is nil")
	}
	user.Name = "hello"
	return nil
}

此函数 UpdateUser 内部调用 updateInferUser 完成对变量的修改。

func TestUpdateUser(t *testing.T) {
	u := User{
		Name:     "1-init",
		Password: "1-init",
	}

	convey.Convey("TestUpdateUser", t, func() {
		convey.Convey("success1", func() {
			patches := gomonkey.NewPatches()
			defer patches.Reset()
			patches.ApplyFunc(UpdateInferUser, func(object *User) error {
				object.Name = "1-update"
				return nil
			})
			err := UpdateUser(&u)
			convey.So(err, convey.ShouldEqual, nil)
		})
	})

}

执行测试用例

3.2 结构体方法测试

type User struct {
	Name     string `json:"name"`
	Password string `json:"password"`
}

// UpdatePassword 修改密码
func (u *User) UpdatePassword(newPassword string) {
	u.Password = newPassword
}

func UpdateUser(user *User) error {
	fmt.Println(user) // 第一次打印user
	user.UpdatePassword("<PASSWORD>")
	fmt.Println(user) //  第二次打印user
	return nil
}

结构体内定义了方法 user.UpdatePassword

测试代码如下

convey.Convey("TestUpdateUser", t, func() {
		convey.Convey("success1", func() {
			patches := gomonkey.NewPatches()
			defer patches.Reset()
			patches.ApplyMethod(reflect.TypeOf(&u), "UpdatePassword", func(_ *User, _newStr string) {
				u.Password = "1234"
			})
			err := UpdateUser(&u)
			convey.So(err, convey.ShouldEqual, nil)
		})
	})

通过 applyMethod进行测试

4.遇到问题

4.1 mac m2 芯片直接执行测试会 permission denied问题

原因是因为 mac m系列对于内存有保护

    err := syscall.Mprotect(page, syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC)
    if err != nil {
        panic(err)
    }

解决方案参考 https://github.com/agiledragon/gomonkey/issues/70

目前看下来有三种

  • 修改gomonkey源代码,err := syscall.Mprotect(page, syscall.PROT_READ|syscall.PROT_WRITE)

  • 设置环境变量 GOARCH=amd64

  • 直接通过docker -v 映射主机文件到docker镜像文件,在镜像中执行。

4.1.1 设置环境变量
GOARCH=amd64 go test --run TestWaitForServiceCreate -gcflags=all=-l

4.1.2 使用docker

docker中执行测试步骤如下

  1. 确保mac系统中以及安装了docker (docker对于大型企业收费,对于个人版还是免费,这里有需要也可以替换为 podman)

  2. 在代码当前目录执行

docker run -it --rm -v $PWD:/opt/www golang:1.20  /bin/bash

3. 进入系统中后切换到 /opt/www 目录执行

go test -v --run TestReadJsonFile  -gcflags=all=-l

结果如下

相关链接


评论