首页
Preview

Golang单元测试中“Mocking”一个函数

图片来源: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

版权声明:本文内容由TeHub注册用户自发贡献,版权归原作者所有,TeHub社区不拥有其著作权,亦不承担相应法律责任。 如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

点赞(0)
收藏(0)
菜鸟一只
你就是个黄焖鸡,又黄又闷又垃圾。

评论(0)

添加评论