Merge pull request #118 from bbx-winner/main

compat: support testcase generated by HttpRunner
This commit is contained in:
debugtalk
2022-03-04 17:28:20 +08:00
committed by GitHub
13 changed files with 389 additions and 15 deletions

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
@@ -29,6 +30,10 @@ func loadFromJSON(path string) (*TCase, error) {
decoder := json.NewDecoder(bytes.NewReader(file))
decoder.UseNumber()
err = decoder.Decode(tc)
if err != nil {
return tc, err
}
err = convertCompatTestCase(tc)
return tc, err
}
@@ -48,9 +53,83 @@ func loadFromYAML(path string) (*TCase, error) {
tc := &TCase{}
err = yaml.Unmarshal(file, tc)
if err != nil {
return tc, nil
}
err = convertCompatTestCase(tc)
return tc, err
}
func convertCompatTestCase(tc *TCase) (err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("convert compat testcase error: %v", p)
}
}()
for _, step := range tc.TestSteps {
// 1. deal with request body compatible with HttpRunner
if step.Request != nil && step.Request.Body == nil {
if step.Request.Json != nil {
step.Request.Headers["Content-Type"] = "application/json; charset=utf-8"
step.Request.Body = step.Request.Json
} else if step.Request.Data != nil {
step.Request.Body = step.Request.Data
}
}
// 2. deal with validators compatible with HttpRunner
for i, iValidator := range step.Validators {
validatorMap := iValidator.(map[string]interface{})
validator := Validator{}
_, checkExisted := validatorMap["check"]
_, assertExisted := validatorMap["assert"]
_, expectExisted := validatorMap["expect"]
// check priority: HRP > HttpRunner
if checkExisted && assertExisted && expectExisted {
// HRP validator format
validator.Check = validatorMap["check"].(string)
validator.Assert = validatorMap["assert"].(string)
validator.Expect = validatorMap["expect"]
if msg, existed := validatorMap["msg"]; existed {
validator.Message = msg.(string)
}
validator.Check = convertCheckExpr(validator.Check)
step.Validators[i] = validator
} else if len(validatorMap) == 1 {
// HttpRunner validator format
for assertMethod, iValidatorContent := range validatorMap {
checkAndExpect := iValidatorContent.([]interface{})
if len(checkAndExpect) != 2 {
return fmt.Errorf("unexpected validator format: %v", validatorMap)
}
validator.Check = checkAndExpect[0].(string)
validator.Assert = assertMethod
validator.Expect = checkAndExpect[1]
}
validator.Check = convertCheckExpr(validator.Check)
step.Validators[i] = validator
} else {
return fmt.Errorf("unexpected validator format: %v", validatorMap)
}
}
}
return err
}
// convertCheckExpr deals with check expression including hyphen
func convertCheckExpr(checkExpr string) string {
if strings.Contains(checkExpr, textExtractorSubRegexp) {
return checkExpr
}
checkItems := strings.Split(checkExpr, ".")
for i, checkItem := range checkItems {
if strings.Contains(checkItem, "-") && !strings.Contains(checkItem, "\"") {
checkItems[i] = fmt.Sprintf("\"%s\"", checkItem)
}
}
return strings.Join(checkItems, ".")
}
func (tc *TCase) ToTestCase() (*TestCase, error) {
testCase := &TestCase{
Config: tc.Config,

View File

@@ -34,3 +34,30 @@ func TestLoadCase(t *testing.T) {
t.Fail()
}
}
func Test_convertCheckExpr(t *testing.T) {
exprs := []struct {
before string
after string
}{
// normal check expression
{"a.b.c", "a.b.c"},
{"headers.\"Content-Type\"", "headers.\"Content-Type\""},
// check expression using regex
{"covering (.*) testing,", "covering (.*) testing,"},
{" (.*) a-b-c", " (.*) a-b-c"},
// abnormal check expression
{"-", "\"-\""},
{"b-c", "\"b-c\""},
{"a.b-c.d", "a.\"b-c\".d"},
{"a-b.c-d", "\"a-b\".\"c-d\""},
{"\"a-b\".c-d", "\"a-b\".\"c-d\""},
{"headers.Content-Type", "headers.\"Content-Type\""},
{"body.I-am-a-Key.name", "body.\"I-am-a-Key\".name"},
}
for _, expr := range exprs {
if !assert.Equal(t, convertCheckExpr(expr.before), expr.after) {
t.Fail()
}
}
}

View File

@@ -1,8 +1,9 @@
# Release History
## v0.6.3 (2022-02-22)
## v0.6.3 (2022-03-03)
- feat: support customized setup/teardown hooks (variable assignment not supported)
- compat: support testcase generated by HttpRunner
## v0.6.2 (2022-02-22)

25
examples/compat_test.go Normal file
View File

@@ -0,0 +1,25 @@
package examples
import (
"testing"
"github.com/httprunner/hrp"
)
// generated by examples/har/demo.har using HttpRunner v3.1.6
const demoHttpRunnerJSONPath = "demo_httprunner.json"
const demoHttpRunnerYAMLPath = "demo_httprunner.yaml"
func TestCompatTestCase(t *testing.T) {
testcaseFromJSON := &hrp.TestCasePath{Path: demoHttpRunnerJSONPath}
err := hrp.NewRunner(t).Run(testcaseFromJSON)
if err != nil {
t.Fatalf("run testcase error: %v", err)
}
testcaseFromYAML := &hrp.TestCasePath{Path: demoHttpRunnerYAMLPath}
err = hrp.NewRunner(t).Run(testcaseFromYAML)
if err != nil {
t.Fatalf("run testcase error: %v", err)
}
}

View File

@@ -0,0 +1,135 @@
{
"config": {
"name": "testcase description",
"variables": {},
"verify": false
},
"teststeps": [
{
"name": "/get",
"request": {
"url": "https://postman-echo.com/get",
"params": {
"foo1": "HDnY8",
"foo2": "34.5"
},
"method": "GET",
"headers": {
"Host": "postman-echo.com",
"User-Agent": "HttpRunnerPlus",
"Accept-Encoding": "gzip"
}
},
"validate": [
{
"eq": [
"status_code",
200
]
},
{
"eq": [
"headers.Content-Type",
"application/json; charset=utf-8"
]
},
{
"eq": [
"body.url",
"https://postman-echo.com/get?foo1=HDnY8&foo2=34.5"
]
}
]
},
{
"name": "/post",
"request": {
"url": "https://postman-echo.com/post",
"method": "POST",
"cookies": {
"sails.sid": "s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk"
},
"headers": {
"Host": "postman-echo.com",
"User-Agent": "Go-http-client/1.1",
"Content-Length": "28",
"Content-Type": "application/json; charset=UTF-8",
"Cookie": "sails.sid=s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk",
"Accept-Encoding": "gzip"
},
"json": {
"foo1": "HDnY8",
"foo2": 12.3
}
},
"validate": [
{
"eq": [
"status_code",
200
]
},
{
"eq": [
"headers.Content-Type",
"application/json; charset=utf-8"
]
},
{
"eq": [
"body.url",
"https://postman-echo.com/post"
]
}
]
},
{
"name": "/post",
"request": {
"url": "https://postman-echo.com/post",
"method": "POST",
"cookies": {
"sails.sid": "s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw"
},
"headers": {
"Host": "postman-echo.com",
"User-Agent": "Go-http-client/1.1",
"Content-Length": "20",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Cookie": "sails.sid=s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw",
"Accept-Encoding": "gzip"
},
"data": {
"foo1": "HDnY8",
"foo2": "12.3"
}
},
"validate": [
{
"eq": [
"status_code",
200
]
},
{
"eq": [
"headers.Content-Type",
"application/json; charset=utf-8"
]
},
{
"eq": [
"body.data",
""
]
},
{
"eq": [
"body.url",
"https://postman-echo.com/post"
]
}
]
}
]
}

View File

@@ -0,0 +1,81 @@
config:
name: testcase description
variables: {}
verify: false
teststeps:
- name: /get
request:
headers:
Accept-Encoding: gzip
Host: postman-echo.com
User-Agent: HttpRunnerPlus
method: GET
params:
foo1: HDnY8
foo2: '34.5'
url: https://postman-echo.com/get
validate:
- eq:
- status_code
- 200
- eq:
- headers.Content-Type
- application/json; charset=utf-8
- eq:
- body.url
- https://postman-echo.com/get?foo1=HDnY8&foo2=34.5
- name: /post
request:
cookies:
sails.sid: s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk
headers:
Accept-Encoding: gzip
Content-Length: '28'
Content-Type: application/json; charset=UTF-8
Cookie: sails.sid=s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk
Host: postman-echo.com
User-Agent: Go-http-client/1.1
json:
foo1: HDnY8
foo2: 12.3
method: POST
url: https://postman-echo.com/post
validate:
- eq:
- status_code
- 200
- eq:
- headers.Content-Type
- application/json; charset=utf-8
- eq:
- body.url
- https://postman-echo.com/post
- name: /post
request:
cookies:
sails.sid: s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw
data:
foo1: HDnY8
foo2: '12.3'
headers:
Accept-Encoding: gzip
Content-Length: '20'
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Cookie: sails.sid=s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw
Host: postman-echo.com
User-Agent: Go-http-client/1.1
method: POST
url: https://postman-echo.com/post
validate:
- eq:
- status_code
- 200
- eq:
- headers.Content-Type
- application/json; charset=utf-8
- eq:
- body.data
- ''
- eq:
- body.url
- https://postman-echo.com/post

View File

@@ -7,6 +7,8 @@ import (
"github.com/httprunner/hrp/internal/builtin"
)
const rendezvousTestJSONPath = "rendezvous_test.json"
var rendezvousTestcase = &hrp.TestCase{
Config: hrp.NewConfig("run request with functions").
SetBaseURL("https://postman-echo.com").
@@ -59,7 +61,7 @@ func TestRendezvousDump2JSON(t *testing.T) {
if err != nil {
t.Fatalf("ToTCase error: %v", err)
}
err = builtin.Dump2JSON(tCase, "rendezvous_test.json")
err = builtin.Dump2JSON(tCase, rendezvousTestJSONPath)
if err != nil {
t.Fatalf("dump to json error: %v", err)
}

View File

@@ -145,7 +145,7 @@ func (h *har) prepareTestStep(entry *Entry) (*hrp.TStep, error) {
step := &tStep{
TStep: hrp.TStep{
Request: &hrp.Request{},
Validators: make([]hrp.Validator, 0),
Validators: make([]interface{}, 0),
},
}
if err := step.makeRequestMethod(entry); err != nil {

View File

@@ -4,6 +4,8 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/httprunner/hrp"
)
var (
@@ -98,13 +100,16 @@ func TestMakeTestCase(t *testing.T) {
}
// make validators
if !assert.Equal(t, "status_code", tCase.TestSteps[0].Validators[0].Check) {
validator, ok := tCase.TestSteps[0].Validators[0].(hrp.Validator)
if !ok || !assert.Equal(t, "status_code", validator.Check) {
t.Fail()
}
if !assert.Equal(t, "headers.\"Content-Type\"", tCase.TestSteps[0].Validators[1].Check) {
validator, ok = tCase.TestSteps[0].Validators[1].(hrp.Validator)
if !ok || !assert.Equal(t, "headers.\"Content-Type\"", validator.Check) {
t.Fail()
}
if !assert.Equal(t, "body.url", tCase.TestSteps[0].Validators[2].Check) {
validator, ok = tCase.TestSteps[0].Validators[2].(hrp.Validator)
if !ok || !assert.Equal(t, "body.url", validator.Check) {
t.Fail()
}
}

View File

@@ -97,6 +97,8 @@ type Request struct {
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`
Cookies map[string]string `json:"cookies,omitempty" yaml:"cookies,omitempty"`
Body interface{} `json:"body,omitempty" yaml:"body,omitempty"`
Json interface{} `json:"json,omitempty" yaml:"json,omitempty"`
Data interface{} `json:"data,omitempty" yaml:"data,omitempty"`
Timeout float32 `json:"timeout,omitempty" yaml:"timeout,omitempty"`
AllowRedirects bool `json:"allow_redirects,omitempty" yaml:"allow_redirects,omitempty"`
Verify bool `json:"verify,omitempty" yaml:"verify,omitempty"`
@@ -122,7 +124,7 @@ type TStep struct {
SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"`
TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"`
Extract map[string]string `json:"extract,omitempty" yaml:"extract,omitempty"`
Validators []Validator `json:"validate,omitempty" yaml:"validate,omitempty"`
Validators []interface{} `json:"validate,omitempty" yaml:"validate,omitempty"`
Export []string `json:"export,omitempty" yaml:"export,omitempty"`
}
@@ -301,11 +303,11 @@ type testCaseInOut struct {
type testCaseSummary struct {
Name string `json:"name" yaml:"name"`
Success bool `json:"success" yaml:"success"`
CaseId string `json:"case_id,omitempty" yaml:"case_id,omitempty"` //TODO
CaseId string `json:"case_id,omitempty" yaml:"case_id,omitempty"` // TODO
Stat *testStepStat `json:"stat" yaml:"stat"`
Time *testCaseTime `json:"time" yaml:"time"`
InOut *testCaseInOut `json:"in_out" yaml:"in_out"`
Log string `json:"log,omitempty" yaml:"log,omitempty"` //TODO
Log string `json:"log,omitempty" yaml:"log,omitempty"` // TODO
Records []*stepData `json:"records" yaml:"records"`
}
@@ -330,7 +332,7 @@ type address struct {
type SessionData struct {
Success bool `json:"success" yaml:"success"`
ReqResps *reqResps `json:"req_resps" yaml:"req_resps"`
Address *address `json:"address,omitempty" yaml:"address,omitempty"` //TODO
Address *address `json:"address,omitempty" yaml:"address,omitempty"` // TODO
Validators []*validationResult `json:"validators,omitempty" yaml:"validators,omitempty"`
}

View File

@@ -114,8 +114,12 @@ func (v *responseObject) Extract(extractors map[string]string) map[string]interf
return extractMapping
}
func (v *responseObject) Validate(validators []Validator, variablesMapping map[string]interface{}) (err error) {
for _, validator := range validators {
func (v *responseObject) Validate(iValidators []interface{}, variablesMapping map[string]interface{}) (err error) {
for _, iValidator := range iValidators {
validator, ok := iValidator.(Validator)
if !ok {
return errors.New("validator type error")
}
// parse check value
checkItem := validator.Check
var checkValue interface{}
@@ -160,6 +164,7 @@ func (v *responseObject) Validate(validators []Validator, variablesMapping map[s
}
v.validationResults = append(v.validationResults, validResult)
log.Info().
Str("checkExpr", validator.Check).
Str("assertMethod", assertMethod).
Interface("expectValue", expectValue).
Interface("checkValue", checkValue).
@@ -168,7 +173,8 @@ func (v *responseObject) Validate(validators []Validator, variablesMapping map[s
if !result {
v.t.Fail()
return errors.New(fmt.Sprintf(
"do assertion failed, assertMethod: %v, checkValue: %v, expectValue: %v",
"do assertion failed, checkExpr: %v, assertMethod: %v, checkValue: %v, expectValue: %v",
validator.Check,
assertMethod,
checkValue,
expectValue,

View File

@@ -703,6 +703,15 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro
if err != nil {
return stepResult, err
}
// check request body format if Content-Type specified as application/json
if strings.HasPrefix(req.Header.Get("Content-Type"), "application/json") {
switch data.(type) {
case bool, float64, string, map[string]interface{}, []interface{}, nil:
break
default:
return stepResult, errors.Errorf("request body type inconsistent with Content-Type: %v", req.Header.Get("Content-Type"))
}
}
requestMap["body"] = data
var dataBytes []byte
switch vv := data.(type) {

View File

@@ -42,7 +42,8 @@ func TestRunRequestGetToStruct(t *testing.T) {
if tStep.Request.Cookies["user"] != "debugtalk" {
t.Fatalf("tStep.Request.Cookies mismatch")
}
if tStep.Validators[0].Check != "status_code" || tStep.Validators[0].Expect != 200 {
validator, ok := tStep.Validators[0].(Validator)
if !ok || validator.Check != "status_code" || validator.Expect != 200 {
t.Fatalf("tStep.Validators mismatch")
}
}
@@ -67,7 +68,8 @@ func TestRunRequestPostDataToStruct(t *testing.T) {
if tStep.Request.Body != "a=1&b=2" {
t.Fatalf("tStep.Request.Data mismatch")
}
if tStep.Validators[0].Check != "status_code" || tStep.Validators[0].Expect != 200 {
validator, ok := tStep.Validators[0].(Validator)
if !ok || validator.Check != "status_code" || validator.Expect != 200 {
t.Fatalf("tStep.Validators mismatch")
}
}