婚纱影楼网站建设,手机网站生成小程序,永久短链接,莱芜新闻直播目录
背景
框架
ginkgo初始化
抓包运行脚本
目录说明
∮./business
∮./conf
∮./utils
∮./testcase
testcase 用例目录结构规则
示例
实现思路
解析Har数据
定义结构体
解析到json
转换请求数据
转换请求
转换请求参数
写业务请求数据
写gotest测试…目录
背景
框架
ginkgo初始化
抓包运行脚本
目录说明
∮./business
∮./conf
∮./utils
∮./testcase
testcase 用例目录结构规则
¶示例
实现思路
解析Har数据
定义结构体
解析到json
转换请求数据
转换请求
转换请求参数
写业务请求数据
写gotest测试用例数据
初始化写入suit文件
格式化测试文件
install生成的业务请求目录
格式化响应断言
可能遇到的问题
完整代码
详细代码如下注释已经给得比较清晰 资料获取方法 背景
之前写过一篇博客介绍怎么用Python通过解析抓包数据完成自动化用例的编写。最近这段时间在使用go test所以就在想能不能也使用代码来生成自动化用例快速提升测试用例覆盖率。说干就干。 框架
首先介绍一下我们使用的测框架
项信息安装备注GO版本go1.12.9 darwin/amd64略测试框架ginkgogo get -u github.com/onsi/ginkgo/ginkgo断言库testify/assertgo 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层目录层级
¶示例
软件测试论坛项目组/论坛项目/帖子模块/创建帖子接口 CN_TestBBS/bbs/post/post_test.go基础账号项目/首页项目/白名单接口 CN_account/homepage/whitelist_test.go 实现思路
按照har文件的JSON结构定义对应的结构体然后解析数据生成请求数据生成断言数据初始化测试套suite格式化代码初始化包引用信息。
解析Har数据
定义结构体
Log struct {version stringcreator stringEntries []struct {startedDateTime stringtime stringRequest 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.Textif 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)))returndefault:// fmt.Println(k, is of a type didnt 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.Textif 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 stringreturndefault:formatParam genGetParam(interfaceName, postReqParam)// fmt.Println(k, is of a type didnt 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 mainimport (encoding/base64encoding/jsonfmtosos/execpath/filepathstrings
)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)// 格式化请求参数为标准请求stringgetReqParam : 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.Textif v.Request.Method GET {paramstr genGetParam(InterfaceName, getReqParam)}if v.Request.Method POST {paramstr genPostParam(InterfaceName, postReqParamStr)}// 格式化接口返回数据为标准断言stringtext, _ : 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 didnt 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 .goTestcasefilename : 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()// 导入InterfaceFilepathexec.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 .gofilepath : 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.gofilepath : testCaseBaseFolder / path /os.MkdirAll(filepath, 0777)file, err os.Create(filepath filename)return
}func initTestsuit(serverName string) {filename : serverName _suite_test.gofilepath : testCaseBaseFolder / serverName /os.MkdirAll(filepath, 0777)file, err : os.Create(filepath filename)if err ! nil {fmt.Println(initTestsuit Error, err)}// Testsuite后的 首字母需大写否则suite无法正常检索到testcasefile.WriteString(fmt.Sprintf(package %s_testimport (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 ParammapDeepCopy 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.EntireHeaderParamassert : 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() {%sret, 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 : %surl : CN_bbs.TESTSERVERDOMAIN pathret, 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)))returndefault:// fmt.Println(k, is of a type didnt 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 stringreturndefault:formatParam genGetParam(interfaceName, postReqParam)// fmt.Println(k, is of a type didnt 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 stringvv : []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 stringcreator stringEntries []struct {startedDateTime stringtime stringRequest struct {Method stringURL stringhttpVersion stringCookies []stringHeaders []struct {Name stringValue string}QueryString []struct {Name stringValue string}PostData struct {MimeType stringText string}headersSize int32bodySize int32}Response struct {_charlesStatus stringStatus int32StatusText stringhttpVersion stringcookies []stringHeaders []struct {Name stringValue string}Content struct {size int32mimeType stringText stringEncoding string}redirectURL stringheadersSize intbodySize int}serverIPAddress stringcache map[string]stringtimings map[string]int32}}
}// UnpackHar 解析 har
func UnpackHar(har []byte) (logs *Har) {err : json.Unmarshal(har, logs)if err ! nil {fmt.Println(err)}return
}文中可能存在描述不正确欢迎大神们指正补充
感谢阅读如果觉得对你有帮助就在右下角点个赞吧感谢
合抱之木生于毫末九层之台起于累土千里之行始于足下。 资料获取方法
【留言777】 各位想获取源码等教程资料的朋友请点赞 评论 收藏三连
三连之后我会在评论区挨个私信发给你们~