【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格式,保存在当前目录下
    • 如何安装抓包工具在本文就不赘述了,抓包,过滤出想要的数据,导出,保存的格式注意选择为har
  • 根据实际情况修改全局变量信息,如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数据

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

转换请求数据

转换请求
转换请求参数

GET

// 格式化请求参数为标准请求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)
}
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

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后方的字符需为大写的字母,否则服务无法被发现,所以需要做大小写转换

完整代码

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

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
}
posted @ 2019-03-12 14:08  Bingo-he  阅读(2665)  评论(2编辑  收藏  举报