Golang unit testing and mock summary

I. Introduction

1. Positioning of single test

      The status of single testing in software engineering is unquestionable. It requires engineers to actively think about code boundaries, exception handling, etc. On the other hand, it is the best description of the code. What your function does specifically, the input and output are clear at a glance.

      Computer scientist Edsger Dijkstra once said: "Testing can prove the existence of defects, but cannot prove the absence of defects." No amount of testing can prove that a program is bug-free. At their best, testing can increase our confidence that the code will work correctly in many important scenarios.

Reference: Go Language Bible Test Function

2. Generate single test in vscode

Reference: Quickly generate unit tests in VS Code

      vscodeGenerating unit tests is as follows. We need to write an array of test cases, clearly indicate the want results and wantErr, and execute the test case array by traversing.

func TestGenerateStsTokenService(t *testing.T) {
    
    
	type args struct {
    
    
		ctx             context.Context
		generateStsData *dto.GenerateStsReqParams
	}
	tests := []struct {
    
    
		name     string
		args     args
		wantResp *common.RESTResp
		wantErr  bool
	}{
    
    
		{
    
    
			name: "测试正常生成sts",
			args: args{
    
    
				ctx: context.TODO(),
				generateStsData: &dto.GenerateStsReqParams{
    
    
					SessionName: "webApp",
					AuthParams:  &dto.AuthParamsData{
    
    },
				},
			},
			wantResp: &common.RESTResp{
    
    
				Code: 0,
				Data: &dto.OssStsRespData{
    
    
				},
			},
			wantErr: false,
		},
		{
    
    
			name: "测试异常生成sts",
			args: args{
    
    
				ctx: context.TODO(),
				generateStsData: &dto.GenerateStsReqParams{
    
    
					SessionName: "liteApp",
					AuthParams:  &dto.AuthParamsData{
    
    },
				},
			},
			wantResp: &common.RESTResp{
    
    
				Code: 20003,
				Data: interface{
    
    }(nil),
			},
			wantErr: true,
		},
	}
	for _, tt := range tests {
    
    
		t.Run(tt.name, func(t *testing.T) {
    
    
 
			gotResp, err := GenerateStsTokenService(tt.args.ctx, tt.args.generateStsData)
			if (err != nil) != tt.wantErr {
    
    
				t.Errorf("GenerateStsTokenService() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(gotResp, tt.wantResp) {
    
    
				t.Errorf("GenerateStsTokenService() = %v, want %v", gotResp, tt.wantResp)
			}
		})
	}
}

2. Things to note when constructing test cases

1. Project initialization

// TestMain会在执行其他测试用例的时候,自动执行
func TestMain(m *testing.M) {
    
    
    setup()  //初始化函数
    retCode := m.Run() // 运行单元测试
    teardown() //后置校验,钩子函数,可不实现
    os.Exit(retCode) //清理结果
}

2. Construct empty interface{}

// 直接给Data赋值为nil的话,验证会失败,
// 单纯的nil和(*infra.QueryOneMappingCode)(nil)是不一样的
wantResp: &common.RESTResp{
    
    
				Code:    0,
				Message: "",
				Data:    (*infra.QueryOneMappingCode)(nil),
			},

// 数组类型的空
// []dto.OneMappingCode{}也会验证失败
wantRes: []dto.OneMappingCode(nil),

3. Construct the time.Time type of the structure

Data: &infra.xxx{
    
    
					ID:          54,
					Code:        "338798",
					TakerUid:    "",
					State:       1,
					Type:        1,
					CreatedAt: time.Date(2023, time.June, 9, 16, 32, 59, 0, time.Local),
				},

也可以直接打印接口的返回,看看CreatedAt返回的是什么,然后构造一下就可以。
t.Logf("gotResp:(%#v)", gotResp.Data)

4. Construct test case in json format

wantResp: &common.RESTResp{
    
    
				Code:    0,
				Message: "success",
				Data: `{
					"id": 54,
					"code": "338798",
					"creator_uid": "12345",
					"client_appId": "1234",
					"taker_uid": "",
					"state": 1,
					"type": 1,
					"created_at": "2023-06-09T16:32:59+08:00"
				   }`,
			},

3. Run the single test file

1. Run the single test file as a whole

  cd /xxx 单测目录
  go test
  成功输出:
  PASS
  ok

2. Report an error when running a single test file

The error message is as follows:

# command-line-arguments [command-line-arguments.test]
./base_test.go:26:18: undefined: Ping

      Obviously Pingthe function and single test files are under the same package, why does it appear undefined? command-line-argumentsWhat is it?
answer:

(1) What are command-line-arguments?
go test [flags] [packages] [build flags] [packages]
命令行参数中指定的每个包或文件都将被视为一个要进行测试的包。而 "command-line-arguments" 
这个标识符就是用来表示上述情况中命令行参数中指定的文件。

这样可以使 go test 命令将指定的文件作为单独的包进行处理,并执行其中的测试函数。
(2) Cause of undefined occurrence

The error prompts that the build failed, which means that we need to pass in the files that the single test file depends on. For example, if I test the base_test.go file here alone, I need to write base.go into the command line parameters.
Specific reference: [Golang] Solve the problem of undefined prompt when Go test executes a single test file

go test ./base.go ./base_test.go
(3) Panic occurs due to lack of initialization

Generally speaking, we just need to define a TestMain() function under a package to initialize the code. But when we need to run a single test file, there may be no TestMain() in the test file.

api_test.go
	TestMain()
base_test.go // 没有TestMain()函数

// 解决方案
1、初始化代码放到setup()函数中
2go命令行
go test ./base.go ./base_test.go ./api_test.go ./api.go
3、只想运行base_test.go怎么办
	base_test.go中加上自己的setuoBase()

3. Check the single test coverage rate

go test -cover
	coverage: 80.4% of statements

4. Interpretation of single test coverage files

go test -coverprofile=coverage.out

// 打开单测覆盖率文件
mode: set
base.go:10.118,14.23 3 1
base.go:14.23,17.3 2 1

	解释如下:
	10.118,14.23 3 1 表示第 10 行到第 14 行代码被测试覆盖到了,且覆盖
	率为 3/1 (300%)。这是因为第 10 行至少执行了一次,如果执行了三次,则覆盖率为 300%14.23,17.3 2 1 表示第 14 行到第 17 行代码被测试覆盖到了,且覆盖率为 2/1 (200%)

5. Generate a single test file that can be opened by the browser

go test -coverprofile=coverage.out
go tool cover -html=coverage.out -o coverage.html

Green represents code that is covered, red represents code that is not covered.
The upper left corner is the coverage of all go files in the directory where the single test command is run.
You can consider adding a single test case to cover this part of red.
Insert image description here

6. Issues with single test coverage

      Coverage 100%means that the test case covers all possible execution paths, that is, all functions of the program are covered. A coverage higher than 100%means that the same code path is tested multiple times or that certain lines of code are executed multiple times during the period being tested.

      However, single testing 100%并cannot guarantee that there will be no bugproblems. It can only guarantee that the written code has no problems, but logical or business vulnerabilities cannot be detected.

      The blogger's group in Didi recommends single test coverage 50%or above. Other friends' companies require that the core interface must have single test and the overall single test coverage 30%is above. Please refer to it if necessary.

4. Issues regarding single measurement granularity

      When writing a single test, I always wonder how detailed it should be? Especially when the original project did not have a single test, the code to supplement the single test was more than the business logic code. . .
In this example, the directory structure is as follows:

domain:
	base.go
	code.go
	code_test.go
	util.go

code.goFunctions that will call base.goand will be found. When running, it is found that the single test coverage is already available. Does this mean that we only need to write one ?util.gocode_test.go
80%code_test.go

1. chatgpt’s answer

      In fact, it is not, base.goand util.goit may be used by other files later. When we write a single test, we should try to cover all abnormal situations, that is, the boundary issues of the program. Therefore , we base.goalso util.goneed to do corresponding unit tests, so as to obtain high-quality code.

2. Personal understanding

      The problem caused by a single code_test.gofile is that the underlying functions mockmay affect the actual data, resulting in a single test that can only be run once, not all the time PASS. Secondly, the length of the code process leads to casemore and more single tests being written, which is close to integration testing. This is not the goal of our single tests.

      After removing all the functions code_test.gorelated to base.goand from the test , we found that the single test coverage was only low and the test path was relatively short. You also need to write and util_test.go separately, and the single test coverage will be achieved immediately after writing .util.gomock37%base_test.goutil_test.go82%

      The granularity of the split becomes finer and more attention is paid to the input and output of each function. Especially when modifying a function, you only need to use the corresponding single test for verification, instead of testing from the entrance. After all, unit tests are not integration tests.

Reference:
Golang unit testing: What are the misunderstandings and practices?
Unit testing tips for Go

5. Mock data

      When writing a single test, the program will inevitably have various cross-file function calls, as well as the operation of third-party middleware or upstream and downstream interactions. This is particularly mockimportant at this time.

      Imagine that mockwhen there is no such thing, when we run a single test, it may be written to the database once? Or make a request to the downstream? Such a single test can only be run once. mockThe emergence of allows us to pay attention to the implementation details of the code, without worrying about data pollution or the situation where a single test can only run once and GG.

1. Mock component selection

Reference: How to do unit testing well? Golang Mock "Three Musketeers" gomock, monkey, sqlmock
GO advanced unit testing

Insert image description here

      The blogger here prefers a non-intrusive approach mock, just take the plunge. It's a pity that monkeyit is no longer updated. Now it is all gomonkeydeveloped by a Chinese boss.

gomonkey project library
analysis Golang test (8) - gomonkey practical combat

2. Mock practical operation

(1) Mock function call

      There are a large number of encapsulated calls in functions, such as this A->B, A->Cso freedom mock Band Cfunctions are still very important for our unit testing.

patches := gomonkey.ApplyFunc(queryOneMappCode, func(ctx context.Context, code string) (*infra.QueryOneMappingCode, error) {
    
    
				// 参数大于6则返回空
				if len(code) > 6 {
    
    
					return nil, nil
				}
				return &infra.QueryOneMappingCode{
    
    
					ID:          54,
					Code:        "338798",
					CreatedAt:   time.Date(2023, time.June, 9, 16, 32, 59, 0, time.Local),
				}, nil
			})
			defer patches.Reset()
(2) Mock method call
1、实例化接口
var mockProvider = provider.Test
// 接口如下
type TestDbProvider interface {
    
    
	SetDb(db *sqlx.DB)
	GetOne(dest interface{
    
    }, sql string, args interface{
    
    }) (resp *infra.QueryOneMappingCode, err error)
}


2、mock对应的查询方法
// 注意,第一个参数不能是指针,不然mock会失效
// 例如 var oss_bucket_obj *oss.Bucket ,传入target为: *oss_bucket_obj
// 传地址会报错
patches := gomonkey.ApplyMethodFunc(mockProvider, "GetOne", func(dest interface{
    
    }, sql string, args interface{
    
    }) (resp *infra.QueryOneMappingCode, err error) {
    
    
				code := args.(string)
				if code == "123456" {
    
    
					return &infra.QueryOneMappingCode{
    
    
						ID:          1,
						Code:        "123456",
						CreatedAt:   time.Date(2023, time.June, 9, 16, 32, 59, 0, time.Local),
					}, nil
				} else if code == "456789" {
    
    
					return &infra.QueryOneMappingCode{
    
    
						ID:          1,
						Code:        "456789",
						CreatedAt:   time.Date(2023, time.June, 9, 16, 32, 59, 0, time.Local),
					}, nil
				} else {
    
    
					return nil, nil
				}
			})
			defer patches.Reset()
(3) Mock functions of other packages

Just reference other packages directly in xx_testthe file. Generally xx_test.go, they xx.goare under the same package, so there is no need to worry about circular references.

patches := gomonkey.ApplyFunc(util.GenerateRandomCode, func(numDigits int) string {
    
    
				return "123456"
			})
			defer patches.Reset()
(4) Functions in mock loop

For example, in function A, if function B is called three times in a loop, the mock is as follows:

createA := &infra.CreateMappingCode{
    
    Code: "933903"}
			createB := &infra.CreateMappingCode{
    
    Code: "601690"}
			createC := &infra.CreateMappingCode{
    
    Code: "798493"}
			p := gomonkey.ApplyFuncSeq(structureMappingCodeRecord, []gomonkey.OutputCell{
    
    
				{
    
    Values: gomonkey.Params{
    
    createA}},
				{
    
    Values: gomonkey.Params{
    
    createB}},
				{
    
    Values: gomonkey.Params{
    
    createC}},
			})
			defer p.Reset() // 恢复原始函数
(5)Mock http call
// vscode自动生成的test代码
for _, tt := range tests {
    
    
		t.Run(tt.name, func(t *testing.T) {
    
    
			// mock httptest
			ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    
    
				if r.Method != http.MethodGet {
    
    
					w.WriteHeader(http.StatusNotFound)
				}
				// 构造返回参数
				w.WriteHeader(http.StatusOK)
				// 获取POST请求的参数,根据参数返回不同的响应
				bodyBytes, err := io.ReadAll(r.Body)
				if err != nil {
    
    
					// 处理错误
					w.WriteHeader(http.StatusBadRequest)
				}
				// 获取post参数
				params := new(dto.GenerateStsReqParams)
				json.Unmarshal(bodyBytes, params)
				// 根据传递的参数返回不同的响应
				res := new(common.RESTResp)
				if params.SessionName == "webApp" {
    
    
					res = &common.RESTResp{
    
    
						Code:    0,
						Message: "success",
						Data: &dto.OssStsRespData{
    
    
							Region:          "hangzhou",
							Bucket:          "test",
						},
					}
				} else {
    
    
					res = &common.RESTResp{
    
    
						Code:    1,
						Message: "failed",
						Data:    &dto.OssStsRespData{
    
    },
					}
				}
				// 模拟接口的返回,http接口返回是字节数据,因此需要json.Marshal
				jsonStr, _ := json.Marshal(res)
				w.Write(jsonStr)
			}))
			defer ts.Close()
			// 替换原来的url为mock的url
			GenerateOssStsUrl = ts.URL
    	// 发起请求,请求中的http会被mock掉
			gotResp, err := GenerateStsTokenService(tt.args.ctx, tt.args.generateStsData)
			if (err != nil) != tt.wantErr {
    
    
				t.Errorf("GenerateStsTokenService() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			t.Logf("gotResp:(%#v) ,wantResp:(%#v)", gotResp, tt.wantResp)
			if !reflect.DeepEqual(gotResp, tt.wantResp) {
    
    
				t.Errorf("GenerateStsTokenService() = %v, want %v", gotResp, tt.wantResp)
			}
		})
	}

3. Opinions on mocking

mockThere are two attitudes towards

一方的人主张不要滥用mock,能不mock就不mock。被测单元也不一定是具体的一个
函数,可能是多个函数本来就应该串起来,必要的时候再mock。

一方则主张将被测函数所有调用的外面函数全部mock掉,只关注被测函数自己的
一行行代码,只要调用其他函数,全都mock掉,用假数据来测试。

Originally from the perspective of laziness and less writing of single tests, I supported the first method.

例如:
单测函数:A函数
内部逻辑:
	A->B : B函数全是业务逻辑
	A->C : C函数包括mysql或者redis操作
	A->D->E: D函数纯业务逻辑,构造请求参数。E函数对外发起http请求

      The first way is to only mock Csum Ethe function. AWhen testing the function, the sum will Balso Dbe tested. Mainly a trouble-free and quick way.

      Until I encountered a more complex scenario, Bwhere there are B1sum B2functions, Dand there are D1sum D2functions, and the logic was very complex, the first method became integration testing. Single test cases slowly turned into test cases. D2For example , when only modifying a function, you need to modify and Atest it through a single test. . . .

mockThe second way is to make external calls       at each layer . The single test Aonly focuses Aon the logic, mockand B,C,D,Eonly focuses on B,C,D,Ewhether the output is correct or wrong.
There is also its own single test function for B,C,D,Ethe function, which is fully covered. In this way, when modifying D2the function, you only need to modify and pass D2the single test.

      For external dependencies, such as third-party libraries, mysql,redis,mqthis is done uniformly mock. For internal function calls, it is recommended to be more granular and A_test.goonly be A.goresponsible for the logic inside. As for the calling B.gopart, leave it to me B_test.go.

end

Guess you like

Origin blog.csdn.net/LJFPHP/article/details/131742158