目录

【Golang】基于录制,自动生成go test接口自动化用例

背景

之前写过一篇博客,介绍怎么用Python通过解析抓包数据,完成自动化用例的编写。最近这段时间在使用go test,所以就在想能不能也使用代码来生成自动化用例,快速提升测试用例覆盖率。说干就干。

框架

首先介绍一下我们使用的测框架:

信息 安装 备注
GO版本 go1.12.9 darwin/amd64
测试框架 ginkgo go get -u github.com/onsi/ginkgo/ginkgo
断言库 testify/assert go get github.com/stretchr/testify 官方配套的断言库是gomega

ginkgo初始化

  • 初始化: cd path/to/package/you/want/to/test && ginkgo bootstrap
  • 创建示例用例:ginkgo generate (需要手动添加测试用例)
  • 运行测试: go testor ginkgo

注:-v加上参数可打印运行信息

抓包&运行脚本

  • 使用抓包工具(如Charles)抓包,把数据包导出为har格式,保存在当前目录下
    • 如何安装抓包工具在本文就不赘述了,抓包,过滤出想要的数据,导出,保存的格式注意选择为har1172048-20190814103753708-830223471.png
  • 根据实际情况修改全局变量信息,如bizBaseFolder、serverName、userFile等
  • 使用go run gentest.go运行脚本即可

目录说明

然后我们一起来了解一下我们的目录结构定义。

∮./business

业务封装,封装具体的请求及测试数据

∮./conf

配置信息及接口请求参数初始化封装

∮./utils

公共函数封装

∮./testcase

接口测试用例目录

testcase 用例目录结构规则

基本原则: 根据项目、模块、接口功能逐级区分,建议最多3层目录层级

¶示例
  1. 软件测试论坛项目组/论坛项目/帖子模块/创建帖子接口:
    • CN_TestBBS/bbs/post/post_test.go
  2. 基础账号项目/首页项目/白名单接口:
    • CN_account/homepage/whitelist_test.go

实现思路

按照har文件的JSON结构定义对应的结构体,然后解析数据,生成请求数据,生成断言数据,初始化测试套suite,格式化代码,初始化包引用信息。

解析Har数据

定义结构体
1
2
3
4
5
6
7
8
Log struct {
		version string
		creator string
		Entries []struct {
			startedDateTime string
			time            string
			Request         struct {
				...
解析到json
1
2
3
4
5
6
7
func UnpackHar(har []byte) (logs *Har) {
	err := json.Unmarshal(har, &logs)
	if err != nil {
		fmt.Println(err)
	}
	return
}

转换请求数据

转换请求
转换请求参数

GET

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 格式化请求参数为标准请求string
getReqParam := make(map[string]interface{}, 1)
if len(v.Request.QueryString) > 0 {
    for _, query := range v.Request.QueryString {
        getReqParam[query.Name] = query.Value
    }
}
// 获取postReq数据
postReqParamStr := v.Request.PostData.Text

if v.Request.Method == "GET" {
    paramstr = genGetParam(InterfaceName, getReqParam)
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func genGetParam(interfaceName string, param map[string]interface{}) (formatParam string) {

	// 对于请求参数的value值为 数组
	if len(param) > 0 {
		for k, v := range param {
			switch vv := v.(type) {
			case []interface{}:
				fmt.Sprintf(k, "is an array:", vv)
				temp, _ := json.Marshal(param)
				formatParam = fmt.Sprintf("%sParam = `%s`", interfaceName, fmt.Sprintf("%v", string(temp)))
				return
			default:
				// fmt.Println(k, "is of a type didn't handle")
			}
		}
	}
	temp, _ := json.Marshal(param)
	formatParam = fmt.Sprintf(`%sParam = map[string]interface{} %s`, interfaceName, fmt.Sprintf("%v", string(temp)))
	return
}

POST

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
postReqParamStr := v.Request.PostData.Text
    if v.Request.Method == "POST" {
    paramstr = genPostParam(InterfaceName, postReqParamStr)
}
func genPostParam(interfaceName string, postReqParamStr string) (formatParam string) {
	// formatParam = fmt.Sprintf(`%sParam = map[string]interface{} %s`, interfaceName, param)
	// fmt.Sprintf("%v", string(temp))
	postReqParam := make(map[string]interface{}, 1)

	if len(postReqParamStr) > 0 {
		// 判断第一个字符是否为{}, 做传递数据为数组[]的兼容
		if []rune(postReqParamStr)[0] == '{' {
			var x interface{}
			err := json.Unmarshal([]byte(postReqParamStr), &x)
			if err != nil {
				fmt.Println("err", err)
			}

			postReqParam = x.(map[string]interface{})
			// fmt.Println(postReqParam)
			// 判断value中是否存在数组
			for k, v := range postReqParam {
				switch vv := v.(type) {
				// switch vv := v.(type) {
				case []interface{}:
					fmt.Sprintf(k, "is an array:", vv)
					// param[k] = fmt.Sprintf("`%s`", vv)
					temp, _ := json.Marshal(postReqParam)
					formatParam = fmt.Sprintf("%sParam = `%s`", interfaceName, fmt.Sprintf("%v", string(temp)))
					paramType = "string"
					return
				default:
					formatParam = genGetParam(interfaceName, postReqParam)
					// fmt.Println(k, "is of a type didn't handle")
				}
			}
			// 如果为数组,做如下处理
		} else {
			var y []interface{}
			err := json.Unmarshal([]byte(postReqParamStr), &y)
			if err != nil {
				fmt.Println("err", err)
			}

			postReqParam = y[0].(map[string]interface{})
			temp, _ := json.Marshal(postReqParam)

			// 声明请求类型
			paramType = "[]map[string]interface{}"
			formatParam = fmt.Sprintf(`%sParam =[]map[string]interface{}{%s}`, interfaceName, string(temp))
			// 无法使用 判断类型 Param := utils.MapDeepCopy(Hebinz123.XlppcPlaylistApiV1RemarkDelParam)
		}
	}
	// temp, _ := json.Marshal(param)
	// formatParam = fmt.Sprintf(`%sParam = map[string]interface{} %s`, interfaceName, fmt.Sprintf("%v", string(temp)))
	return
}

写业务请求数据

写gotest测试用例数据

格式化请求参数为标准请求string。

初始化写入suit文件

这里有一个注意点,Test后紧接的数据必须是大写。

格式化测试文件

使用goimports库初始化导入数据包。

install生成的业务请求目录

使用go install目录生成导入业务请求目录

格式化响应断言

使用类型判断格式化接口返回数据为标准断言string。

可能遇到的问题

  • 初始化读取文件的存储buf的size和其实际大小不一致时,json 解析出错“invalid character ‘\x00’ after top-level value”
  • go install 执行失败,导致测试用例无法找到其依赖包
  • get请求,post请求参数在har文件中的存储方式不一致,获取数据的方式差别很大
  • 域名及接口命名规则不一致,-.等等风格不一致
  • 测试suite 紧接Test后方的字符需为大写的字母,否则服务无法被发现,所以需要做大小写转换

完整代码

详细代码如下,注释已经给得比较清晰:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
package main

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

var (
	baseDomain         = "test.bbs.com"    // 测试域名,用于切割出请求路径
	bizBaseFolder      = "business/CN_bbs" //业务请求目录
	testCaseBaseFolder = "testcase/CN_bbs" // 测试用例目录
	serverName         = "cinecismGo"      // 服务名
	paramType          = ""
)

func main() {
	userFile := "20190917-cinecismgo.har" // 抓包文件地址
	fl, err := os.Open(userFile)
	if err != nil {
		fmt.Println(userFile, err)
		return
	}
	defer fl.Close()

	// 读取har数据
	fileInfo, err := fl.Stat()
	buf := make([]byte, fileInfo.Size()) // “invalid character '\x00' after top-level value”
	fl.Read(buf)
	data := UnpackHar(buf)

	for _, v := range data.Log.Entries {
		// 每一个循环初始化请求参数类型
		paramType = "map[string]interface{}"
		paramstr := ""

		// 初始化 请求path,生成标准请求接口名称
		pathStr, path := initPath(v.Request.URL)
		InterfaceName := formatInterfaceName(pathStr)

		// 格式化请求参数为标准请求string
		getReqParam := make(map[string]interface{}, 1)
		if len(v.Request.QueryString) > 0 {
			for _, query := range v.Request.QueryString {
				getReqParam[query.Name] = query.Value
			}
		}
		// 获取postReq数据
		postReqParamStr := v.Request.PostData.Text

		if v.Request.Method == "GET" {
			paramstr = genGetParam(InterfaceName, getReqParam)
		}
		if v.Request.Method == "POST" {
			paramstr = genPostParam(InterfaceName, postReqParamStr)
		}

		// 格式化接口返回数据为标准断言string
		text, _ := base64.StdEncoding.DecodeString(v.Response.Content.Text)
		responseAssertStr := initAssert(text)

		// 创建业务请求文件、测试用例文件
		run(serverName, path, InterfaceName, v.Request.Method, responseAssertStr, paramstr)

		// 【待补充】handle Headers数据
		// fmt.Println(initHeaders(data))
	}
}

func initAssert(text []byte) (responseAssertStr string) {
	if len(text) > 0 {
		var Response interface{}
		err := json.Unmarshal(text, &Response)
		if err != nil {
			fmt.Println("err", err)
		}

		responseMap := Response.(map[string]interface{})
		res := []string{}
		for k, v := range responseMap {
			switch vv := v.(type) {
			case string:
				// fmt.Println(k, "is string", vv)
				res = append(res, fmt.Sprintf("%s, _ := js.Get(\"%s\").String() \n assert.Equal(%s, `%v`)", k, k, k, string(vv)))
			case int64:
				// fmt.Println(k, "is int", vv)
				res = append(res, fmt.Sprintf("%s, _ := js.Get(\"%s\").Int() \n assert.Equal(%s, %v)", k, k, k, string(vv)))
			case float64:
				// fmt.Println(k, "is float64", vv)
				res = append(res, fmt.Sprintf("%s, _ := js.Get(\"%s\").Int() \n assert.Equal(%s, %v)", k, k, k, vv))
			case bool:
				// fmt.Println(k, "is bool", vv)
				res = append(res, fmt.Sprintf("%s, _ := js.Get(\"%s\").Bool() \n assert.Equal(%s, %v)", k, k, k, vv))
			case []interface{}:
				// fmt.Println(k, "is an array:", vv)
				res = append(res, fmt.Sprintf("// Key【%s】的子层级的value值未生成断言,系多层级数组数据,具体值如下:", k))
				res = append(res, fmt.Sprintf("// %v ", vv))
			case map[string]interface{}:
				// fmt.Println(k, "is an map:", vv)
				temp, _ := json.Marshal(vv)
				res = append(res, fmt.Sprintf("// Key【%s】的子层级value值未生成断言,系多层级Map数据,具体值如下:", k))
				res = append(res, fmt.Sprintf("// %v ", string(temp)))
			default:
				// fmt.Println(k, "is of a type didn't handle", vv)
			}
			responseAssertStr = strings.Join(res, "\n")
		}
	}
	return
}

func initPath(URL string) (pathStr, path string) {
	pathStr = strings.Split(URL, baseDomain)[1]
	if strings.Contains(pathStr, "?") {
		pathStr = strings.Split(pathStr, "?")[0]
		path = strings.Split(pathStr, "?")[0]
	} else {
		path = pathStr
	}
	if strings.Contains(pathStr, ".") {
		pathStr = strings.Replace(pathStr, ".", "/", 10)
		pathStr = strings.Replace(pathStr, "-", "/", 10)
	}
	// fmt.Println(path)
	// fmt.Println("pathStr", pathStr)
	return
}

func run(serverName, path, InterfaceName, method, responseAssertStr string, Param string) {
	// 初始化测试文件
	InterfaceFilepath := filepath.Join(bizBaseFolder, serverName)
	Testcasefilepath := filepath.Join(testCaseBaseFolder, serverName)
	InterfaceFileame := InterfaceName + ".go"
	Testcasefilename := InterfaceName + "_test.go"

	// 创建并写入标准请求信息
	file, err := createFile(InterfaceFilepath, InterfaceFileame)
	if err != nil {
		fmt.Println("createInterfaceFile", err)
	}
	writeParam(file, serverName, []string{Param})
	writeReq(file, InterfaceName, path, method)
	defer file.Close()

	// 创建并写入测试用例信息
	file1, err := createFile(Testcasefilepath, Testcasefilename)
	if err != nil {
		fmt.Println("createTestcasefile", err)
	}

	// 写入suit文件
	initTestsuit(serverName)

	// 写入测试用例
	writeTestcase(file1, serverName, InterfaceName, responseAssertStr)
	defer file1.Close()

	// 格式化测试文件
	exec.Command("goimports", "-w", InterfaceFilepath).Run()
	exec.Command("goimports", "-w", Testcasefilepath).Run()

	// 导入InterfaceFilepath
	exec.Command("go", "install", InterfaceFilepath).Run()
}

func initHeaders(har *Har) map[string]string {
	var headers = make(map[string]string)
	// fmt.Println(len(har.Log.Entries[0].Request.Headers))

	for _, v := range har.Log.Entries[0].Request.Headers {
		headers[v.Name] = v.Value
	}
	return headers
}

func createFile(filepaths, filename string) (file *os.File, err error) {
	os.MkdirAll(filepaths, 0777)
	file, err = os.Create(filepath.Join(filepaths, filename))
	return
}

func createInterfaceFile(path, filename string) (file *os.File, err error) {
	filename = filename + ".go"
	filepath := bizBaseFolder + "/" + path + "/"
	os.MkdirAll(filepath, 0777)
	file, err = os.Create(filepath + filename)
	return
}

func createTestcasefile(path, filename string) (file *os.File, err error) {
	filename = filename + "_test.go"
	filepath := testCaseBaseFolder + "/" + path + "/"
	os.MkdirAll(filepath, 0777)
	file, err = os.Create(filepath + filename)
	return
}

func initTestsuit(serverName string) {
	filename := serverName + "_suite_test.go"
	filepath := testCaseBaseFolder + "/" + serverName + "/"
	os.MkdirAll(filepath, 0777)
	file, err := os.Create(filepath + filename)
	if err != nil {
		fmt.Println("initTestsuit Error", err)
	}
	// Testsuite后的 首字母需大写,否则suite无法正常检索到testcase
	file.WriteString(fmt.Sprintf(
		`package %s_test

		import (
			"testing"

			. "github.com/onsi/ginkgo"
			. "github.com/onsi/gomega"
		)

		func Test%s(t *testing.T) {
			RegisterFailHandler(Fail)
			RunSpecs(t, "%s Suite")
		}`, serverName, Capitalize(serverName), serverName))
}

func writeTestcase(file *os.File, serverName, InterfaceName, responseAssertStr string) {
	// 接口引入路径 【服务名称.接口名称】
	interfaceImportPath := serverName + "." + InterfaceName
	// 接口标准请求参数 【接口名称Param】
	paramImportPath := interfaceImportPath + "Param"

	// 接口标准请求参数拷贝,请求参数为非标准【map[string]interface{}】类型时,该参数为空
	tempParamStr := ""
	// 是否使用mapDeepCopy,请求参数为非标准【map[string]interface{}】类型时 使用
	mapDeepCopy := ""
	if paramType != "map[string]interface{}" {
		tempParamStr = paramImportPath
	}

	if paramType == "map[string]interface{}" {
		tempParamStr = "Param"
		mapDeepCopy = fmt.Sprintf(`Param := utils.MapDeepCopy(%s)`, paramImportPath)
	}

	// fmt.Println("---------------->", paramType)
	file.WriteString(fmt.Sprintf("package %s_test\n\n", serverName))
	file.WriteString(`import . "github.com/onsi/ginkgo"`)
	file.WriteString("\n\n")
	file.WriteString(fmt.Sprintf(`var _ = Describe("%s", func() {
		headers := common.EntireHeaderParam
		assert := assert.New(GinkgoT())
		BeforeEach(func() {
			By("begin test")
		})
		JustBeforeEach(func() {
			By("just say start")
		})
		AfterEach(func() {
			By("end test")
		})
		Context("%s", func() {
			It("正常%s", func() {
				%s
				ret, resp, _ := %s(%s, headers)
				assert.Equal(ret.StatusCode, 200)
				js, errs := simplejson.NewJson(resp)
				if errs != nil {
					panic(errs)
				}
				%s
			})
		})
	})`, serverName, InterfaceName, InterfaceName, mapDeepCopy, interfaceImportPath, tempParamStr, responseAssertStr))
}

func writeParam(file *os.File, serverName string, params []string) {
	file.WriteString(fmt.Sprintf("package %s", serverName))
	file.WriteString("\n\n\n")
	file.WriteString("var (")
	for _, param := range params {
		file.WriteString(param)
	}
	file.WriteString(")")
	file.WriteString("\n\n\n")
}

func writeReq(file *os.File, InterfaceName, path, method string) {
	file.WriteString(fmt.Sprintf(`func %s(param %s, header map[string]string) (ret gorequest.Response, content []byte, result string) {
		path := "%s"
		url := CN_bbs.TESTSERVERDOMAIN + path
		ret, content = common.Common%s(url, param, header)
		fmt.Println(ret.Request.URL)
		// js, _ := simplejson.NewJson([]byte(content))
		//result, _ = js.Get("result").String()
		return
	}`, InterfaceName, paramType, path, method))
}

func genGetParam(interfaceName string, param map[string]interface{}) (formatParam string) {

	// 对于请求参数的value值为 数组
	if len(param) > 0 {
		for k, v := range param {
			switch vv := v.(type) {
			case []interface{}:
				fmt.Sprintf(k, "is an array:", vv)
				temp, _ := json.Marshal(param)
				// 如果是数组格式,直接当作字符串处理(map[]interface{}格式无法表示该类型参数)
				formatParam = fmt.Sprintf("%sParam = `%s`", interfaceName, fmt.Sprintf("%v", string(temp)))
				return
			default:
				// fmt.Println(k, "is of a type didn't handle")
			}
		}
	}
	temp, _ := json.Marshal(param)
	formatParam = fmt.Sprintf(`%sParam = map[string]interface{} %s`, interfaceName, fmt.Sprintf("%v", string(temp)))
	return
}

func genPostParam(interfaceName string, postReqParamStr string) (formatParam string) {
	// formatParam = fmt.Sprintf(`%sParam = map[string]interface{} %s`, interfaceName, param)
	// fmt.Sprintf("%v", string(temp))
	postReqParam := make(map[string]interface{}, 1)

	if len(postReqParamStr) > 0 {
		// 判断第一个字符是否为{}, 做传递数据为数组[]的兼容
		if []rune(postReqParamStr)[0] == '{' {
			var x interface{}
			err := json.Unmarshal([]byte(postReqParamStr), &x)
			if err != nil {
				fmt.Println("err", err)
			}

			postReqParam = x.(map[string]interface{})
			// fmt.Println(postReqParam)
			// 判断value中是否存在数组
			for k, v := range postReqParam {
				switch vv := v.(type) {
				// switch vv := v.(type) {
				case []interface{}:
					fmt.Sprintf(k, "is an array:", vv)
					// param[k] = fmt.Sprintf("`%s`", vv)
					temp, _ := json.Marshal(postReqParam)
					formatParam = fmt.Sprintf("%sParam = `%s`", interfaceName, fmt.Sprintf("%v", string(temp)))
					paramType = "string"
					return
				default:
					formatParam = genGetParam(interfaceName, postReqParam)
					// fmt.Println(k, "is of a type didn't handle")
				}
			}
			// 如果为数组,做如下处理
		} else {
			var y []interface{}
			err := json.Unmarshal([]byte(postReqParamStr), &y)
			if err != nil {
				fmt.Println("err", err)
			}

			postReqParam = y[0].(map[string]interface{})
			temp, _ := json.Marshal(postReqParam)

			// 声明请求类型
			paramType = "[]map[string]interface{}"
			formatParam = fmt.Sprintf(`%sParam =[]map[string]interface{}{%s}`, interfaceName, string(temp))
			// 无法使用 判断类型 Param := utils.MapDeepCopy(Hebinz123.CNppcPlaylistApiV1RemarkDelParam)
		}
	}
	// temp, _ := json.Marshal(param)
	// formatParam = fmt.Sprintf(`%sParam = map[string]interface{} %s`, interfaceName, fmt.Sprintf("%v", string(temp)))
	return
}

func formatInterfaceName(path string) (InterfaceName string) {
	paths := strings.Split(path, "/")

	for k, v := range paths {
		paths[k] = Capitalize(v)
	}

	InterfaceName = strings.Join(paths, "")
	return
}

// Capitalize 字符首字母大写
func Capitalize(str string) string {
	var upperStr string
	vv := []rune(str)
	for i := 0; i < len(vv); i++ {
		if i == 0 {
			if vv[i] >= 97 && vv[i] <= 122 { // 判断是否是小写字母
				vv[i] -= 32 // string的码表相差32位
				upperStr += string(vv[i])
			} else {
				fmt.Println("Not begins with lowercase letter,")
				return str
			}
		} else {
			upperStr += string(vv[i])
		}
	}
	return upperStr
}

// Har Logs 解析
type Har struct {
	Log struct {
		version string
		creator string
		Entries []struct {
			startedDateTime string
			time            string
			Request         struct {
				Method      string
				URL         string
				httpVersion string
				Cookies     []string
				Headers     []struct {
					Name  string
					Value string
				}
				QueryString []struct {
					Name  string
					Value string
				}
				PostData struct {
					MimeType string
					Text     string
				}
				headersSize int32
				bodySize    int32
			}
			Response struct {
				_charlesStatus string
				Status         int32
				StatusText     string
				httpVersion    string
				cookies        []string
				Headers        []struct {
					Name  string
					Value string
				}
				Content struct {
					size     int32
					mimeType string
					Text     string
					Encoding string
				}
				redirectURL string
				headersSize int
				bodySize    int
			}
			serverIPAddress string
			cache           map[string]string
			timings         map[string]int32
		}
	}
}

// UnpackHar 解析 har
func UnpackHar(har []byte) (logs *Har) {
	err := json.Unmarshal(har, &logs)
	if err != nil {
		fmt.Println(err)
	}
	return
}