zoukankan      html  css  js  c++  java
  • 【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() 
     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() 
     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() 
     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() 
     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, "
    ")
    		}
    	}
    	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
    
    ", serverName))
    	file.WriteString(`import . "github.com/onsi/ginkgo"`)
    	file.WriteString("
    
    ")
    	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("
    
    
    ")
    	file.WriteString("var (")
    	for _, param := range params {
    		file.WriteString(param)
    	}
    	file.WriteString(")")
    	file.WriteString("
    
    
    ")
    }
    
    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
    }
    
  • 相关阅读:
    Qt回车键提交文本代码
    Qt滚动条样式
    如何在Windows下使用WebMatrix+IIS开发PHP程序
    MySql 字符串时间转换
    缩略图生成代码
    网络文件下载
    字符串替换Replace仅替换第一个匹配项
    asp.net页面button按钮防止重复提交的方法
    对象判等规则
    python 实现远程上传文件夹
  • 原文地址:https://www.cnblogs.com/Detector/p/10516345.html
Copyright © 2011-2022 走看看