mirror of
https://github.com/httprunner/httprunner.git
synced 2026-09-05 07:26:55 +08:00
feat: validate response object
This commit is contained in:
@@ -47,7 +47,7 @@ func TestCaseHardcode(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := httpboomer.Test(testcase)
|
err := httpboomer.Test(t, testcase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("run testcase error: %v", err)
|
t.Fatalf("run testcase error: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ require (
|
|||||||
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef // indirect
|
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/imroc/req v0.3.0 // indirect
|
github.com/imroc/req v0.3.0 // indirect
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
github.com/myzhan/boomer v1.6.0
|
github.com/myzhan/boomer v1.6.0
|
||||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||||
github.com/shirou/gopsutil v3.21.8+incompatible // indirect
|
github.com/shirou/gopsutil v3.21.8+incompatible // indirect
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
|||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/imroc/req v0.3.0 h1:3EioagmlSG+z+KySToa+Ylo3pTFZs+jh3Brl7ngU12U=
|
github.com/imroc/req v0.3.0 h1:3EioagmlSG+z+KySToa+Ylo3pTFZs+jh3Brl7ngU12U=
|
||||||
github.com/imroc/req v0.3.0/go.mod h1:F+NZ+2EFSo6EFXdeIbpfE9hcC233id70kf0byW97Caw=
|
github.com/imroc/req v0.3.0/go.mod h1:F+NZ+2EFSo6EFXdeIbpfE9hcC233id70kf0byW97Caw=
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||||
|
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/myzhan/boomer v1.6.0 h1:xjgvmhDjgU9IEKnB7nU1HyoVEfj8SuuU3u6oY3Nugj0=
|
github.com/myzhan/boomer v1.6.0 h1:xjgvmhDjgU9IEKnB7nU1HyoVEfj8SuuU3u6oY3Nugj0=
|
||||||
@@ -42,5 +45,6 @@ golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 h1:J27LZFQBFoihqXoegpscI10HpjZ7B5WQLLKL2FZXQKw=
|
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 h1:J27LZFQBFoihqXoegpscI10HpjZ7B5WQLLKL2FZXQKw=
|
||||||
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ type TRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TValidator struct {
|
type TValidator struct {
|
||||||
Check string // get value with jmespath
|
Check string // get value with jmespath
|
||||||
Comparator string
|
Assert string
|
||||||
Expect interface{}
|
Expect interface{}
|
||||||
Message string
|
Message string
|
||||||
}
|
}
|
||||||
|
|
||||||
type TStep struct {
|
type TStep struct {
|
||||||
|
|||||||
+109
@@ -1 +1,110 @@
|
|||||||
package httpboomer
|
package httpboomer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/imroc/req"
|
||||||
|
"github.com/jmespath/go-jmespath"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var assertFunctionsMap = map[string]func(t assert.TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool{
|
||||||
|
"equals": assert.EqualValues,
|
||||||
|
"equal": assert.EqualValues, // alias for equals
|
||||||
|
"greater_than": assert.Greater,
|
||||||
|
"less_than": assert.Less,
|
||||||
|
"greater_or_equals": assert.GreaterOrEqual,
|
||||||
|
"less_or_equals": assert.LessOrEqual,
|
||||||
|
"not_equal": assert.NotEqual,
|
||||||
|
"contains": assert.Contains,
|
||||||
|
"regex_match": assert.Regexp,
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResponseObject(t *testing.T, resp *req.Resp) *ResponseObject {
|
||||||
|
// prepare response headers
|
||||||
|
headers := make(map[string]string)
|
||||||
|
for k, v := range resp.Response().Header {
|
||||||
|
if len(v) > 0 {
|
||||||
|
headers[k] = v[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare response cookies
|
||||||
|
cookies := make(map[string]string)
|
||||||
|
for _, cookie := range resp.Response().Cookies() {
|
||||||
|
cookies[cookie.Name] = cookie.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse response body
|
||||||
|
var body interface{}
|
||||||
|
if err := json.Unmarshal(resp.Bytes(), &body); err != nil {
|
||||||
|
log.Fatalf("[NewResponseObject] json.Unmarshal response body err: %v, body: %v",
|
||||||
|
err, string(resp.Bytes()))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
respObjMeta := respObjMeta{
|
||||||
|
StatusCode: resp.Response().StatusCode,
|
||||||
|
Headers: headers,
|
||||||
|
Cookies: cookies,
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert respObjMeta to interface{}
|
||||||
|
respObjMetaBytes, _ := json.Marshal(respObjMeta)
|
||||||
|
var data interface{}
|
||||||
|
if err := json.Unmarshal(respObjMetaBytes, &data); err != nil {
|
||||||
|
log.Fatalf("[NewResponseObject] json.Unmarshal respObjMeta err: %v, respObjMetaBytes: %v",
|
||||||
|
err, string(respObjMetaBytes))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ResponseObject{
|
||||||
|
t: t,
|
||||||
|
respObjMeta: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type respObjMeta struct {
|
||||||
|
StatusCode int `json:"status_code"`
|
||||||
|
Headers map[string]string `json:"headers"`
|
||||||
|
Cookies map[string]string `json:"cookies"`
|
||||||
|
Body interface{} `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResponseObject struct {
|
||||||
|
t *testing.T
|
||||||
|
respObjMeta interface{}
|
||||||
|
validationResults map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ResponseObject) Validate(validators []TValidator) error {
|
||||||
|
for _, validator := range validators {
|
||||||
|
// parse check value
|
||||||
|
checkItem := validator.Check
|
||||||
|
checkValue := v.searchJmespath(checkItem)
|
||||||
|
// get assert method
|
||||||
|
assertMethod := validator.Assert
|
||||||
|
assertFunc := assertFunctionsMap[assertMethod]
|
||||||
|
// parse expected value
|
||||||
|
expectValue := validator.Expect
|
||||||
|
// do assertion
|
||||||
|
result := assertFunc(v.t, expectValue, checkValue)
|
||||||
|
log.Printf("assert %s %s %v => %v", checkItem, assertMethod, expectValue, result)
|
||||||
|
if !result {
|
||||||
|
v.t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ResponseObject) searchJmespath(expr string) interface{} {
|
||||||
|
checkValue, err := jmespath.Search(expr, v.respObjMeta)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[searchJmespath] jmespath.Search error: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return checkValue
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,26 +3,34 @@ package httpboomer
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/imroc/req"
|
"github.com/imroc/req"
|
||||||
)
|
)
|
||||||
|
|
||||||
var defaultRunner = NewRunner()
|
var defaultRunner = NewRunner()
|
||||||
|
|
||||||
func Test(testcases ...*TestCase) error {
|
func Test(t *testing.T, testcases ...*TestCase) error {
|
||||||
return defaultRunner.Run(testcases...)
|
return defaultRunner.WithTestingT(t).Run(testcases...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRunner() *Runner {
|
func NewRunner() *Runner {
|
||||||
return &Runner{
|
return &Runner{
|
||||||
|
t: &testing.T{},
|
||||||
Client: req.New(),
|
Client: req.New(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Runner struct {
|
type Runner struct {
|
||||||
|
t *testing.T
|
||||||
Client *req.Req
|
Client *req.Req
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Runner) WithTestingT(t *testing.T) *Runner {
|
||||||
|
r.t = t
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Runner) Run(testcases ...*TestCase) error {
|
func (r *Runner) Run(testcases ...*TestCase) error {
|
||||||
for _, testcase := range testcases {
|
for _, testcase := range testcases {
|
||||||
if err := r.runCase(testcase); err != nil {
|
if err := r.runCase(testcase); err != nil {
|
||||||
@@ -62,6 +70,7 @@ func (r *Runner) runStep(step IStep, config *TConfig) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Runner) runStepRequest(step *TStep) error {
|
func (r *Runner) runStepRequest(step *TStep) error {
|
||||||
|
// prepare request args
|
||||||
var v []interface{}
|
var v []interface{}
|
||||||
v = append(v, req.Header(step.Request.Headers))
|
v = append(v, req.Header(step.Request.Headers))
|
||||||
v = append(v, req.Param(step.Request.Params))
|
v = append(v, req.Param(step.Request.Params))
|
||||||
@@ -75,12 +84,21 @@ func (r *Runner) runStepRequest(step *TStep) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// do request action
|
||||||
req.Debug = true
|
req.Debug = true
|
||||||
resp, err := r.Client.Do(string(step.Request.Method), step.Request.URL, v...)
|
resp, err := r.Client.Do(string(step.Request.Method), step.Request.URL, v...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer resp.Response().Body.Close()
|
defer resp.Response().Body.Close()
|
||||||
|
|
||||||
|
// validate response
|
||||||
|
respObj := NewResponseObject(r.t, resp)
|
||||||
|
err = respObj.Validate(step.Validators)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -15,12 +15,12 @@ func TestHttpRunner(t *testing.T) {
|
|||||||
GET("/headers").
|
GET("/headers").
|
||||||
Validate().
|
Validate().
|
||||||
AssertEqual("status_code", 200, "check status code").
|
AssertEqual("status_code", 200, "check status code").
|
||||||
AssertEqual("headers.Host", "httpbin.org", "check http response host"),
|
AssertEqual("headers.\"Content-Type\"", "application/json", "check http response Content-Type"),
|
||||||
Step("user-agent").
|
Step("user-agent").
|
||||||
GET("/user-agent").
|
GET("/user-agent").
|
||||||
Validate().
|
Validate().
|
||||||
AssertEqual("status_code", 200, "check status code").
|
AssertEqual("status_code", 200, "check status code").
|
||||||
AssertEqual("body.\"user-agent\"", "python-requests", "check User-Agent"),
|
AssertEqual("headers.\"Content-Type\"", "application/json", "check http response Content-Type"),
|
||||||
Step("TestCase3").CallRefCase(&TestCase{Config: TConfig{Name: "TestCase3"}}),
|
Step("TestCase3").CallRefCase(&TestCase{Config: TConfig{Name: "TestCase3"}}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ func TestHttpRunner(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := Test(testcase1, testcase2)
|
err := Test(t, testcase1, testcase2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("run testcase error: %v", err)
|
t.Fatalf("run testcase error: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-3
@@ -11,7 +11,11 @@ var (
|
|||||||
WithHeaders(map[string]string{"User-Agent": "HttpBoomer"}).
|
WithHeaders(map[string]string{"User-Agent": "HttpBoomer"}).
|
||||||
WithCookies(map[string]string{"user": "debugtalk"}).
|
WithCookies(map[string]string{"user": "debugtalk"}).
|
||||||
Validate().
|
Validate().
|
||||||
AssertEqual("status_code", 200, "check status code")
|
AssertEqual("status_code", 200, "check status code").
|
||||||
|
AssertEqual("headers.Connection", "keep-alive", "check header Connection").
|
||||||
|
AssertEqual("headers.\"Content-Type\"", "application/json; charset=utf-8", "check header Content-Type").
|
||||||
|
AssertEqual("body.args.foo1", "bar1", "check param foo1").
|
||||||
|
AssertEqual("body.args.foo2", "bar2", "check param foo2")
|
||||||
stepPOSTData = Step("post form data").
|
stepPOSTData = Step("post form data").
|
||||||
POST("/post").
|
POST("/post").
|
||||||
WithParams(map[string]interface{}{"foo1": "bar1", "foo2": "bar2"}).
|
WithParams(map[string]interface{}{"foo1": "bar1", "foo2": "bar2"}).
|
||||||
@@ -73,10 +77,10 @@ func TestRunRequestRun(t *testing.T) {
|
|||||||
config := &TConfig{
|
config := &TConfig{
|
||||||
BaseURL: "https://postman-echo.com",
|
BaseURL: "https://postman-echo.com",
|
||||||
}
|
}
|
||||||
if err := defaultRunner.runStep(stepGET, config); err != nil {
|
if err := defaultRunner.WithTestingT(t).runStep(stepGET, config); err != nil {
|
||||||
t.Fatalf("tStep.Run() error: %s", err)
|
t.Fatalf("tStep.Run() error: %s", err)
|
||||||
}
|
}
|
||||||
if err := defaultRunner.runStep(stepPOSTData, config); err != nil {
|
if err := defaultRunner.WithTestingT(t).runStep(stepPOSTData, config); err != nil {
|
||||||
t.Fatalf("tStepPOSTData.Run() error: %s", err)
|
t.Fatalf("tStepPOSTData.Run() error: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-5
@@ -1,6 +1,8 @@
|
|||||||
package httpboomer
|
package httpboomer
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
// implements IStep interface
|
// implements IStep interface
|
||||||
type stepRequestValidation struct {
|
type stepRequestValidation struct {
|
||||||
@@ -9,10 +11,10 @@ type stepRequestValidation struct {
|
|||||||
|
|
||||||
func (s *stepRequestValidation) AssertEqual(jmesPath string, expected interface{}, msg string) *stepRequestValidation {
|
func (s *stepRequestValidation) AssertEqual(jmesPath string, expected interface{}, msg string) *stepRequestValidation {
|
||||||
validator := TValidator{
|
validator := TValidator{
|
||||||
Check: jmesPath,
|
Check: jmesPath,
|
||||||
Comparator: "equals",
|
Assert: "equals",
|
||||||
Expect: expected,
|
Expect: expected,
|
||||||
Message: msg,
|
Message: msg,
|
||||||
}
|
}
|
||||||
s.step.Validators = append(s.step.Validators, validator)
|
s.step.Validators = append(s.step.Validators, validator)
|
||||||
return s
|
return s
|
||||||
|
|||||||
Reference in New Issue
Block a user