上一章节,我们主要实现了基础的并发测试场景的能力。本章节,我们实现一下,如何对响应进行提取,使用正则/json对响应信息提取,并赋值给我们定义的变量。
首先定义一个提取的数据结构。model目录下新建withdraw.go
// Package model -----------------------------
// @file : withdraw.go
// @author : 被测试耽误的大厨
// @contact : 13383088061@163.com
// @time : 2023/8/15 15:59
// -------------------------------------------
package model
type Withdraws struct {
WithdrawList []*Withdraw json:"withdraw_list,omitempty" // 提取式一个列表
}
type Withdraw struct {
IsEnable bool json:"is_enable,omitempty" // 是否启用
Key string json:"key,omitempty" // 赋值给->变量名
Type int32 json:"type,omitempty" // 提取类型: 0: 正则表达式 1: json 2:提取响应头 3:提取响应码 默认是0
Index int json:"index,omitempty" // 需要提取指的下标
Value interface{} json:"value,omitempty" // 提取出来的值
Expression string json:"expression,omitempty" // 表达式
ValueType string json:"value_type,omitempty" // 提取出来的值的类型
}
我们针对提取类型,定义一下常量global/constant/constant.go
// 关联提取类型
const (
RegExtract = 0 // 正则提取
JsonExtract = 1 // json提取
HeaderExtract = 2 // 提取header
CodeExtract = 3 // 响应码提取
)
然后,我们实现一下Withdraw结构体的提取方法model/withdraw.go
// http请求提取
func (wd *Withdraw) WithdrawHttp(resp *fasthttp.Response, variable *sync.Map) {
// 定义一个任意类型的变量,用来接收提取的值
var value interface{}
// 根据提取类型判断
switch wd.Type {
case constant.JsonExtract:
var str string
if resp != nil {
str = string(resp.Body())
}
// json.Withdraw,自己实现的json提取方法
value = wd.jsonWithdraw(str)
case constant.CodeExtract:
if resp != nil {
value = resp.StatusCode()
} else {
value = ""
}
case constant.HeaderExtract:
var str string
if resp != nil {
str = resp.Header.String()
}
value = wd.jsonWithdraw(str)
default:
var str string
if resp != nil {
str = string(resp.Body())
}
value = wd.regexWithdraw(str)
}
variable.Store(wd.Key, value)
}
// json提取
func (wd *Withdraw) jsonWithdraw(source string) interface{} {
gq := gjson.Get(source, wd.Expression)
return gq.Value()
}
// 正则提取
func (wd *Withdraw) regexWithdraw(source string) (value interface{}) {
if wd.Expression == "" || source == "" {
value = ""
return
}
value = tools.FindAllDestStr(source, wd.Expression)
if value == nil || len(value.([][]string)) < 1 {
value = ""
} else {
if len(value.([][]string)[0]) <= 1 {
value = value.([][]string)[0][0]
} else {
value = value.([][]string)[0][1]
}
}
return
}
// header提取,使用正则表达式提取
func (wd *Withdraw) headerWithdraw(source string) (value interface{}) {
if wd.Expression == "" || source == "" {
return ""
}
return tools.MatchString(source, wd.Expression, wd.Index)
}
这样,我们有提取的时候,直接根据提取类型,去使用Withdraws的不同的方法即可。
下面,我们完成http请求的关联提取,首先给HttpRequest结构体添加Withdraws字段,使用Withdraws指针类型
// HttpRequest http请求的结构
type HttpRequest struct {
Url string json:"url" // 接口uri
Method string json:"method" // 接口方法,Get Post Update...
Timeout int64 json:"timeout,omitempty" // 请求超时时长,默认为30秒,防止请求阻塞
Body *Body json:"body,omitempty" // 请求提
Headers []Header json:"headers,omitempty" // 接口请求头
Querys []Query json:"querys,omitempty" // get请求时的url
Cookies []Cookie json:"cookies,omitempty" // cookie
Withdraws *Withdraws json:"withdraws,omitempty"
HttpClientSettings HttpClientSettings json:"http_client_settings" // http客户端配置
}
然后,我们实现HttpRequest结构体的withdraw方法
// http提取
func (hr *HttpRequest) withdraw(resp *fasthttp.Response, variableMap *sync.Map) {
if hr.Withdraws == nil {
return
}
if hr.Withdraws.WithdrawList == nil {
return
}
for _, withdraw := range hr.Withdraws.WithdrawList {
if !withdraw.IsEnable {
continue
}
withdraw.WithdrawHttp(resp, variableMap)
}
}
因为我们需要将提取出来的值存放到一个公共的地方,给其他接口去使用,所以我们使用一个带锁的sync.Map来存储提取出来的变量,这样就避免了map的并发安全问题。
修改HttpRequest结构体的Request方法,修改后如下,http_request.go:
func (hr *HttpRequest) Request(response *TestObjectResponse, variableMap *sync.Map) {
// 使用fasthttp 协程池
// 新建一个http请求
req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
// 新建一个http响应接受服务端的返回
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(resp)
// 新建一个http的客户端, newHttpClient是一个方法,在下面
client := newHttpClient(hr.HttpClientSettings)
// 设置请求方法
hr.methodInit(req)
// 设置header
hr.headerInit(req)
// 设置query
hr.queryInit(req)
// 设置cookie
hr.cookieInit(req)
// 设置url
hr.urlInit(req)
// 设置body
hr.bodyInit(req)
// 设置请求超时时间
hr.setReqTimeout(req)
// 记录开始时间
startTime := time.Now()
// 开始请求
err := client.Do(req, resp)
// 计算响应时间差值
requestTime := time.Since(startTime)
response.RequestTime = requestTime.Milliseconds()
response.Code = resp.StatusCode()
if err != nil {
response.Response = err.Error()
} else {
// 如果响应中有压缩类型,防止压缩类型出现乱码
switch string(resp.Header.ContentEncoding()) {
case "br", "deflate", "gzip":
b, _ := resp.BodyUncompressed()
response.Response = string(b)
default:
response.Response = string(resp.Body())
}
}
// 关联提取
hr.withdraw(resp, variableMap)
}
修改TestObject对象的Dispose方法如下:
// Dispose 测试对象的处理函数,在go语言中 Dispose方法是TestObject对象的方法,其他对象不能使用
func (to TestObject) Dispose(response *TestObjectResponse, variableMap *sync.Map) {
switch to.ObjectType {
case HTTP1: // 由于我们有个多类型,为了方便统计,我们定义好变量,直接进行比对即可
to.HttpRequest.Request(response, variableMap)
return
}
return
}
修改TestScene对象的Dispose方法如下:
func (testScene TestScene) Dispose() {
// 从testScene.TestObjects的最外层开始循环
variableMap := &sync.Map{}
for _, testObject := range testScene.TestObjects {
response := &TestObjectResponse{
Name: testObject.Name,
Id: testObject.Id,
SceneId: testScene.Id,
ItemId: testObject.ItemId,
ObjectType: testObject.ObjectType,
}
testObject.Dispose(response, variableMap)
// 从一层级的list中读取每个TestObject对象
}
}
修改/run/testObject/接口方法,分别添加一个全局sync.Map
func RunTestObject(c *gin.Context) {
// 声明一个TO对象
var testObject model.TestObject
// 接收json格式的请求数据
err := c.ShouldBindJSON(&testObject)
id := uuid.New().String()
// 如果请求格式错误
if err != nil {
log.Logger.Error("请求数据格式错误", err.Error())
common.ReturnResponse(c, http.StatusBadRequest, id, "请求数据格式错误!", err.Error())
return
}
// 使用json包解析以下TO对象, 解析出来为[]byte类型
requestJson, _ := json.Marshal(testObject)
// 打印以下日志, 使用fmt.Sprintf包格式花数据,%s 表示string(requestJson)为字符串类型,如果不确定类型,可以使用%v表示
log.Logger.Debug(fmt.Sprintf("测试对象: %s", string(requestJson)))
response := model.TestObjectResponse{
Name: testObject.Name,
Id: testObject.Id,
ObjectType: testObject.ObjectType,
ItemId: testObject.ItemId,
TeamId: testObject.TeamId,
}
variableMap := &sync.Map{}
// 开始处理TO
testObject.Dispose(&response, variableMap)
variableMap.Range(func(key, value any) bool {
log.Logger.Debug(fmt.Sprintf("提取出来: key: %s, value: %v ", key, value))
return true
})
// 返回响应消息
common.ReturnResponse(c, http.StatusOK, id, "请求成功!", response)
return
}
现在启动我们的服务,我们使用下面的body调用我们的/run/testObject/接口:
接口: 127.0.0.1:8002/engine/run/testObject/
{
"name": "百度",
"id": "12312312312312",
"object_type": "HTTP1.1",
"item_id": "12312312312312",
"team_id": "1231231231231",
"http_request": {
"url": "http://www.baidu.com",
"method": "GET",
"request_timeout": 5,
"headers": [],
"querys": [],
"cookies": [],
"http_client_settings": {},
"withdraws": {
"withdraw_list": [
{
"is_enable": true,
"key": "name",
"type": 0,
"expression": "<meta name=\"description\" content=\"(.*?)\">"
}
]
}
}
}
打印的结果如下:
2023-08-17T11:48:46.961+0800 DEBUG service/object_api.go:43 测试对象: {"name":"百度","id":"12312312312312","object_type":"HTTP1.1","item_id":"12312312312312","team_id":"1231231231231","http_request":{"url":"http://www.baidu.com","method":"GET","withdraws":{"withdraw_list":[{"is_enable":true,"key","expression":"\u003cmeta name=\"description\" content=\"(.*?)\"\u003e"}]},"http_client_settings":{"name":"","no_default_user_agent_header":false,"max_conns_per_host":0,"max_idle_conn_duration":0,"max_conn_duration":0,"read_timeout":0,"write_timeout":0,"disable_header_names_normalizing":false,"disable_path_normalizing":false,"advanced_options":{"tls":{"is_verify":false,"verify_type":0,"ca_cert":""}}}}}
2023-08-17T11:48:47.244+0800 DEBUG service/object_api.go:57 提取出来: key: name, value: 全球领先的中文搜索引擎、致力于让网民更便捷地获取信息,找到所求。百度超过千亿的中文网页数据库,可以瞬间找到相关的搜索结果。
可以看到我们提取出来的值为:“全球领先的中文搜索引擎、致力于让网民更便捷地获取信息,找到所求。百度超过千亿的中文网页数据库,可以瞬间找到相关的搜索结果”,并将其赋值给name变量。