图片来源:Tamara Gak (https://unsplash.com/photos/1vZAezBEADw)
接着上一篇介绍 Golang 单元测试的文章,我们将继续讲解 Go 语言中简单函数的模拟测试。在这里,我们只会讨论两种类型的函数:
- 自己包中的函数
- 从项目外部引入的函数(如库) 因此,我们暂时不会“模拟”属于“结构体”的“方法”。你可能会问,为什么需要模拟函数?我们不能直接调用函数吗?简单来说,如果你的函数包含 API/数据库调用(外部调用),那么在测试环境中,你应该对其进行模拟。
开始
假设你有这样一个函数
type Person struct {
ID int `db:"id" json:"id"`
Name string `db:"name" json:"name"`
BornDate string `db:"born_date" json:"born_date"`
}
type Address struct {
ID int `db:"id" json:"id"`
UserID int `db:"user_id" json:"user_id"`
Street string `db:"street" json:"street"`
PostalCode string `db:"postal_code" json:"postal_code"`
}
type UserProfile struct {
User Person `json:"person"`
Address Address `json:"address"`
}
func GetPersonByID(id int) (*Person, error) {
var user Person
err := db.Get(&user, `SELECT id, name, born_date FROM users WHERE id = ?`, id)
if err != nil {
return nil, err
}
return &user, err
}
func GetPersonAddrByUserID(userID int) (*Address, error) {
var addr Address
err := db.Get(&addr, `SELECT id, user_id, street, postal_code FROM address WHERE user_id = ?`, userID)
if err != nil {
return nil, err
}
return &addr, err
}
func GetUserProfile(userID int) (UserProfile, error) {
var userProfile UserProfile
p, err := GetPersonByID(userID)
if err != nil {
return userProfile, err
}
a, err := GetPersonAddrByUserID(userID)
if err != nil {
return userProfile, err
}
userProfile.User = *p
userProfile.Address = *a
return userProfile, nil
}
func JSONMarshalUserProfile(up UserProfile) ([]byte, error) {
return json.Marshal(up)
}
然后你想要测试这两个函数(因为它们依赖于连接到 DB/外部库的函数),并且你希望它们在测试环境中不会执行这些操作(如果你想控制函数的返回结果):
- GetUserProfile
- JSONMarshalUserProfile 我们不会在这篇文章中涉及模拟数据库函数,因此 GetUserByID 和 GetPersonAddress 数据库模拟不会在此时被覆盖。
你知道你可以将你的函数作为一个变量吗?因此,我们可以像这样创建函数(简化版)
var GetPersonByID = func(id int) (*Person, error) {
var user Person
.... // function content
}var GetPersonAddrByUserID = func(userID int) (*Address, error) {
var addr Address
... // function content
}
修改完函数后,我们可以像这样模拟/覆盖你的函数实现。
GetPersonByID = func(id int) (*Person, error) {
// PUT DIFFERENT IMPLEMENTATION HERE
}
当你编写单元测试时,你可以将它们全部放在一起。
package user
import (
"database/sql"
"errors"
"reflect"
"testing"
)
func Test_GetUserProfile(t *testing.T) {
tests := []struct {
name string
userID int
mockFunc func()
expectedProfile UserProfile
expectingErr bool
}{
{
name: "All success no error",
userID: 10,
mockFunc: func() {
GetPersonByID = func(id int) (*Person, error) {
return &Person{
ID: 10,
BornDate: "1989-12-12",
Name: "Jack",
}, nil
}
GetPersonAddrByUserID = func(userID int) (*Address, error) {
return &Address{
ID: 123,
UserID: 10,
PostalCode: "12320",
Street: "Jl. XYZ, No 24, ABC, EFG",
}, nil
}
},
expectedProfile: UserProfile{
User: Person{
ID: 10,
BornDate: "1989-12-12",
Name: "Jack",
},
Address: Address{
ID: 123,
UserID: 10,
PostalCode: "12320",
Street: "Jl. XYZ, No 24, ABC, EFG",
},
},
expectingErr: false,
},
{
name: "Error in get person by id",
userID: 10,
mockFunc: func() {
GetPersonByID = func(id int) (*Person, error) {
return nil, sql.ErrConnDone
}
GetPersonAddrByUserID = func(userID int) (*Address, error) {
return &Address{
ID: 123,
UserID: 10,
PostalCode: "12320",
Street: "Jl. XYZ, No 24, ABC, EFG",
}, nil
}
},
expectedProfile: UserProfile{},
expectingErr: true,
},
{
name: "Error in get person address by user id",
userID: 10,
mockFunc: func() {
GetPersonByID = func(id int) (*Person, error) {
return &Person{
ID: 10,
BornDate: "1989-12-12",
Name: "Jack",
}, nil
}
GetPersonAddrByUserID = func(userID int) (*Address, error) {
return nil, sql.ErrConnDone
}
},
expectedProfile: UserProfile{},
expectingErr: true,
},
}
// preserve the original function
oriGetPersonByID := GetPersonByID
oriGetPersonAddrByUserID := GetPersonAddrByUserID
for _, tc := range tests {
t.Run(tc.name, func(tt *testing.T) {
tc.mockFunc()
up, err := GetUserProfile(tc.userID)
errExist := err != nil
if tc.expectingErr != errExist {
tt.Errorf("Error expectation not met, want %v, got %v", tc.expectingErr, errExist)
}
if !reflect.DeepEqual(tc.expectedProfile, up) {
tt.Errorf("Error, user profile expectation not met, want %+v, got %+v", tc.expectedProfile, up)
}
})
}
GetPersonByID = oriGetPersonByID
GetPersonAddrByUserID = oriGetPersonAddrByUserID
}
在 mockFunc 中,我们将模拟两个函数(GetPersonByID 和 GetPersonAddrByUserID),以便根据我们的测试用例返回一个常量值。请注意,我们需要保存原始函数,以便在单元测试完成后将变量恢复到其原始实现(第 96、97、115、116 行)。
外部函数
那么关于库/另一个项目包中的函数呢?例如,假设我们有这个函数
func JSONMarshalUserProfile(up UserProfile) ([]byte, error) { return json.Marshal(up)}
我们将采用同样的方法,但稍有不同。我们可以将 json.Marshal 保存到一个变量中,例如:
var marshalJSON = json.Marshal
func JSONMarshalUserProfile(up UserProfile) ([]byte, error) { return marshalJSON(up)}
如果你想测试 JSONMarshalUserProfile,你可以像之前的方法一样轻松地模拟 marshalJSON 函数,因为它只是一个你可以覆盖的变量。
marshalJSON = func(v interface{}) ([]byte, error) { // Put your mocking / testing implementation here
}
正如你所看到的,它也适用于外部包,我知道有些人可能认为这不是一个很好的做法,比如有太多的全局变量或函数(如果你正在使用许多外部函数的方法)。我们将在未来的讨论中涉及如何使用接口进行更清晰的模拟。
译自:https://adityarama1210.medium.com/golang-mocking-a-function-for-unit-testing-497b43ad3409
评论(0)