Merge pull request #133 from xucong053/support-api-layer-for-testcase

feat: support api layer and global headers for testcase #94 #95
This commit is contained in:
xucong053
2022-03-15 14:11:21 +08:00
committed by GitHub
37 changed files with 907 additions and 135 deletions

View File

@@ -251,7 +251,7 @@ func TestCaseDemo(t *testing.T) {
}). }).
GET("/get"). GET("/get").
WithParams(map[string]interface{}{"foo1": "$varFoo1", "foo2": "$varFoo2"}). // request with params WithParams(map[string]interface{}{"foo1": "$varFoo1", "foo2": "$varFoo2"}). // request with params
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}). // request with headers WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}). // request with headers
Extract(). Extract().
WithJmesPath("body.args.foo1", "varFoo1"). // extract variable with jmespath WithJmesPath("body.args.foo1", "varFoo1"). // extract variable with jmespath
Validate(). Validate().

View File

@@ -68,6 +68,8 @@ func (b *HRPBoomer) Quit() {
func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rendezvous) *boomer.Task { func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rendezvous) *boomer.Task {
hrpRunner := NewRunner(nil) hrpRunner := NewRunner(nil)
// set client transport for high concurrency load testing
hrpRunner.SetClientTransport(b.GetSpawnCount(), b.GetDisableKeepAlive(), b.GetDisableCompression())
config := testcase.Config config := testcase.Config
// each testcase has its own plugin process // each testcase has its own plugin process

View File

@@ -25,7 +25,7 @@ func TestBoomerStandaloneRun(t *testing.T) {
NewStep("TestCase3").CallRefCase(&TestCase{Config: NewConfig("TestCase3")}), NewStep("TestCase3").CallRefCase(&TestCase{Config: NewConfig("TestCase3")}),
}, },
} }
testcase2 := &TestCasePath{demoTestCaseJSONPath} testcase2 := &demoTestCaseJSONPath
b := NewBoomer(2, 1) b := NewBoomer(2, 1)
go b.Run(testcase1, testcase2) go b.Run(testcase1, testcase2)

View File

@@ -25,7 +25,8 @@ var boomCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
var paths []hrp.ITestCase var paths []hrp.ITestCase
for _, arg := range args { for _, arg := range args {
paths = append(paths, &hrp.TestCasePath{Path: arg}) path := hrp.TestCasePath(arg)
paths = append(paths, &path)
} }
hrpBoomer := hrp.NewBoomer(spawnCount, spawnRate) hrpBoomer := hrp.NewBoomer(spawnCount, spawnRate)
hrpBoomer.SetRateLimiter(maxRPS, requestIncreaseRate) hrpBoomer.SetRateLimiter(maxRPS, requestIncreaseRate)
@@ -38,6 +39,8 @@ var boomCmd = &cobra.Command{
if prometheusPushgatewayURL != "" { if prometheusPushgatewayURL != "" {
hrpBoomer.AddOutput(boomer.NewPrometheusPusherOutput(prometheusPushgatewayURL, "hrp")) hrpBoomer.AddOutput(boomer.NewPrometheusPusherOutput(prometheusPushgatewayURL, "hrp"))
} }
hrpBoomer.SetDisableKeepAlive(disableKeepalive)
hrpBoomer.SetDisableCompression(disableCompression)
hrpBoomer.EnableCPUProfile(cpuProfile, cpuProfileDuration) hrpBoomer.EnableCPUProfile(cpuProfile, cpuProfileDuration)
hrpBoomer.EnableMemoryProfile(memoryProfile, memoryProfileDuration) hrpBoomer.EnableMemoryProfile(memoryProfile, memoryProfileDuration)
hrpBoomer.Run(paths...) hrpBoomer.Run(paths...)
@@ -56,6 +59,8 @@ var (
cpuProfileDuration time.Duration cpuProfileDuration time.Duration
prometheusPushgatewayURL string prometheusPushgatewayURL string
disableConsoleOutput bool disableConsoleOutput bool
disableCompression bool
disableKeepalive bool
) )
func init() { func init() {
@@ -72,4 +77,6 @@ func init() {
boomCmd.Flags().DurationVar(&cpuProfileDuration, "cpu-profile-duration", 30*time.Second, "CPU profile duration.") boomCmd.Flags().DurationVar(&cpuProfileDuration, "cpu-profile-duration", 30*time.Second, "CPU profile duration.")
boomCmd.Flags().StringVar(&prometheusPushgatewayURL, "prometheus-gateway", "", "Prometheus Pushgateway url.") boomCmd.Flags().StringVar(&prometheusPushgatewayURL, "prometheus-gateway", "", "Prometheus Pushgateway url.")
boomCmd.Flags().BoolVar(&disableConsoleOutput, "disable-console-output", false, "Disable console output.") boomCmd.Flags().BoolVar(&disableConsoleOutput, "disable-console-output", false, "Disable console output.")
boomCmd.Flags().BoolVar(&disableCompression, "disable-compression", false, "Disable compression")
boomCmd.Flags().BoolVar(&disableKeepalive, "disable-keepalive", false, "Disable keepalive")
} }

View File

@@ -23,7 +23,8 @@ var runCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
var paths []hrp.ITestCase var paths []hrp.ITestCase
for _, arg := range args { for _, arg := range args {
paths = append(paths, &hrp.TestCasePath{Path: arg}) path := hrp.TestCasePath(arg)
paths = append(paths, &path)
} }
runner := hrp.NewRunner(nil). runner := hrp.NewRunner(nil).
SetFailfast(!continueOnFailure). SetFailfast(!continueOnFailure).

View File

@@ -13,52 +13,80 @@ import (
"github.com/httprunner/hrp/internal/json" "github.com/httprunner/hrp/internal/json"
) )
func loadFromJSON(path string) (*TCase, error) { func loadFromJSON(path string, structObj interface{}) error {
path, err := filepath.Abs(path) path, err := filepath.Abs(path)
if err != nil { if err != nil {
log.Error().Str("path", path).Err(err).Msg("convert absolute path failed") log.Error().Str("path", path).Err(err).Msg("convert absolute path failed")
return nil, err return err
} }
log.Info().Str("path", path).Msg("load json testcase") log.Info().Str("path", path).Msg("load json")
file, err := os.ReadFile(path) file, err := os.ReadFile(path)
if err != nil { if err != nil {
log.Error().Err(err).Msg("load json path failed") log.Error().Err(err).Msg("load json path failed")
return nil, err return err
} }
tc := &TCase{}
decoder := json.NewDecoder(bytes.NewReader(file)) decoder := json.NewDecoder(bytes.NewReader(file))
decoder.UseNumber() decoder.UseNumber()
err = decoder.Decode(tc) err = decoder.Decode(structObj)
if err != nil { return err
return tc, err
}
err = convertCompatTestCase(tc)
return tc, err
} }
func loadFromYAML(path string) (*TCase, error) { func loadFromYAML(path string, structObj interface{}) error {
path, err := filepath.Abs(path) path, err := filepath.Abs(path)
if err != nil { if err != nil {
log.Error().Str("path", path).Err(err).Msg("convert absolute path failed") log.Error().Str("path", path).Err(err).Msg("convert absolute path failed")
return nil, err return err
} }
log.Info().Str("path", path).Msg("load yaml testcase") log.Info().Str("path", path).Msg("load yaml")
file, err := os.ReadFile(path) file, err := os.ReadFile(path)
if err != nil { if err != nil {
log.Error().Err(err).Msg("load yaml path failed") log.Error().Err(err).Msg("load yaml path failed")
return nil, err return err
} }
tc := &TCase{} err = yaml.Unmarshal(file, structObj)
err = yaml.Unmarshal(file, tc) return err
if err != nil { }
return tc, nil
func convertCompatValidator(Validators []interface{}) (err error) {
for i, iValidator := range 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)
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)
Validators[i] = validator
} else {
return fmt.Errorf("unexpected validator format: %v", validatorMap)
}
} }
err = convertCompatTestCase(tc) return nil
return tc, err
} }
func convertCompatTestCase(tc *TCase) (err error) { func convertCompatTestCase(tc *TCase) (err error) {
@@ -79,42 +107,12 @@ func convertCompatTestCase(tc *TCase) (err error) {
} }
// 2. deal with validators compatible with HttpRunner // 2. deal with validators compatible with HttpRunner
for i, iValidator := range step.Validators { err = convertCompatValidator(step.Validators)
validatorMap := iValidator.(map[string]interface{}) if err != nil {
validator := Validator{} return err
_, 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 return nil
} }
// convertCheckExpr deals with check expression including hyphen // convertCheckExpr deals with check expression including hyphen
@@ -136,14 +134,32 @@ func (tc *TCase) ToTestCase() (*TestCase, error) {
Config: tc.Config, Config: tc.Config,
} }
for _, step := range tc.TestSteps { for _, step := range tc.TestSteps {
if step.Request != nil { if step.APIPath != "" {
testCase.TestSteps = append(testCase.TestSteps, &StepRequestWithOptionalArgs{ refAPI := APIPath(step.APIPath)
step.APIContent = &refAPI
apiContent, err := step.APIContent.ToAPI()
if err != nil {
return nil, err
}
step.APIContent = apiContent
testCase.TestSteps = append(testCase.TestSteps, &StepAPIWithOptionalArgs{
step: step, step: step,
}) })
} else if step.TestCase != nil { } else if step.TestCasePath != "" {
refTestCase := TestCasePath(step.TestCasePath)
step.TestCaseContent = &refTestCase
tc, err := step.TestCaseContent.ToTestCase()
if err != nil {
return nil, err
}
step.TestCaseContent = tc
testCase.TestSteps = append(testCase.TestSteps, &StepTestCaseWithOptionalArgs{ testCase.TestSteps = append(testCase.TestSteps, &StepTestCaseWithOptionalArgs{
step: step, step: step,
}) })
} else if step.Request != nil {
testCase.TestSteps = append(testCase.TestSteps, &StepRequestWithOptionalArgs{
step: step,
})
} else if step.Transaction != nil { } else if step.Transaction != nil {
testCase.TestSteps = append(testCase.TestSteps, &StepTransaction{ testCase.TestSteps = append(testCase.TestSteps, &StepTransaction{
step: step, step: step,
@@ -161,29 +177,63 @@ func (tc *TCase) ToTestCase() (*TestCase, error) {
var ErrUnsupportedFileExt = fmt.Errorf("unsupported testcase file extension") var ErrUnsupportedFileExt = fmt.Errorf("unsupported testcase file extension")
// TestCasePath implements ITestCase interface. // APIPath implements IAPI interface.
type TestCasePath struct { type APIPath string
Path string
func (path *APIPath) ToString() string {
return fmt.Sprintf("%v", *path)
} }
func (path *TestCasePath) ToTestCase() (*TestCase, error) { func (path *APIPath) ToAPI() (*API, error) {
var tc *TCase api := &API{}
var err error var err error
casePath := path.Path apiPath := path.ToString()
ext := filepath.Ext(casePath) ext := filepath.Ext(apiPath)
switch ext { switch ext {
case ".json": case ".json":
tc, err = loadFromJSON(casePath) err = loadFromJSON(apiPath, api)
case ".yaml", ".yml": case ".yaml", ".yml":
tc, err = loadFromYAML(casePath) err = loadFromYAML(apiPath, api)
default: default:
err = ErrUnsupportedFileExt err = ErrUnsupportedFileExt
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
tc.Config.Path = path.Path err = convertCompatValidator(api.Validators)
return api, err
}
// TestCasePath implements ITestCase interface.
type TestCasePath string
func (path *TestCasePath) ToString() string {
return fmt.Sprintf("%v", *path)
}
func (path *TestCasePath) ToTestCase() (*TestCase, error) {
tc := &TCase{}
var err error
casePath := path.ToString()
ext := filepath.Ext(casePath)
switch ext {
case ".json":
err = loadFromJSON(casePath, tc)
case ".yaml", ".yml":
err = loadFromYAML(casePath, tc)
default:
err = ErrUnsupportedFileExt
}
if err != nil {
return nil, err
}
err = convertCompatTestCase(tc)
if err != nil {
return nil, err
}
tc.Config.Path = path.ToString()
testcase, err := tc.ToTestCase() testcase, err := tc.ToTestCase()
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -7,16 +7,21 @@ import (
) )
var ( var (
demoTestCaseJSONPath = "examples/demo.json" demoTestCaseJSONPath TestCasePath = "examples/demo.json"
demoTestCaseYAMLPath = "examples/demo.yaml" demoTestCaseYAMLPath TestCasePath = "examples/demo.yaml"
demoRefAPIYAMLPath TestCasePath = "examples/ref_api_test.yaml"
demoRefTestCaseJSONPath TestCasePath = "examples/ref_testcase_test.json"
demoAPIYAMLPath APIPath = "examples/api/put.yml"
) )
func TestLoadCase(t *testing.T) { func TestLoadCase(t *testing.T) {
tcJSON, err := loadFromJSON(demoTestCaseJSONPath) tcJSON := &TCase{}
tcYAML := &TCase{}
err := loadFromJSON(demoTestCaseJSONPath.ToString(), tcJSON)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
t.Fail() t.Fail()
} }
tcYAML, err := loadFromYAML(demoTestCaseYAMLPath) err = loadFromYAML(demoTestCaseYAMLPath.ToString(), tcYAML)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
t.Fail() t.Fail()
} }

View File

@@ -33,4 +33,4 @@ Copyright 2021 debugtalk
* [hrp run](hrp_run.md) - run API test * [hrp run](hrp_run.md) - run API test
* [hrp startproject](hrp_startproject.md) - create a scaffold project * [hrp startproject](hrp_startproject.md) - create a scaffold project
###### Auto generated by spf13/cobra on 10-Mar-2022 ###### Auto generated by spf13/cobra on 15-Mar-2022

View File

@@ -23,7 +23,9 @@ hrp boom [flags]
``` ```
--cpu-profile string Enable CPU profiling. --cpu-profile string Enable CPU profiling.
--cpu-profile-duration duration CPU profile duration. (default 30s) --cpu-profile-duration duration CPU profile duration. (default 30s)
--disable-compression Disable compression
--disable-console-output Disable console output. --disable-console-output Disable console output.
--disable-keepalive Disable keepalive
-h, --help help for boom -h, --help help for boom
--loop-count int The specify running cycles for load testing (default -1) --loop-count int The specify running cycles for load testing (default -1)
--max-rps int Max RPS that boomer can generate, disabled by default. --max-rps int Max RPS that boomer can generate, disabled by default.
@@ -39,4 +41,4 @@ hrp boom [flags]
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing. * [hrp](hrp.md) - One-stop solution for HTTP(S) testing.
###### Auto generated by spf13/cobra on 10-Mar-2022 ###### Auto generated by spf13/cobra on 15-Mar-2022

View File

@@ -23,4 +23,4 @@ hrp har2case $har_path... [flags]
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing. * [hrp](hrp.md) - One-stop solution for HTTP(S) testing.
###### Auto generated by spf13/cobra on 10-Mar-2022 ###### Auto generated by spf13/cobra on 15-Mar-2022

View File

@@ -34,4 +34,4 @@ hrp run $path... [flags]
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing. * [hrp](hrp.md) - One-stop solution for HTTP(S) testing.
###### Auto generated by spf13/cobra on 10-Mar-2022 ###### Auto generated by spf13/cobra on 15-Mar-2022

View File

@@ -16,4 +16,4 @@ hrp startproject $project_name [flags]
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing. * [hrp](hrp.md) - One-stop solution for HTTP(S) testing.
###### Auto generated by spf13/cobra on 10-Mar-2022 ###### Auto generated by spf13/cobra on 15-Mar-2022

34
examples/api/get.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "",
"request": {
"method": "GET",
"url": "/get",
"params": {
"foo1": "bar1",
"foo2": "bar2"
},
"headers": {
"Postman-Token": "ea19464c-ddd4-4724-abe9-5e2b254c2723"
}
},
"validate": [
{
"check": "status_code",
"assert": "equals",
"expect": 200,
"msg": "assert response status code"
},
{
"check": "headers.\"Content-Type\"",
"assert": "equals",
"expect": "application/json; charset=utf-8",
"msg": "assert response header Content-Type"
},
{
"check": "body.url",
"assert": "equals",
"expect": "https://postman-echo.com/get?foo1=bar1&foo2=bar2",
"msg": "assert response body url"
}
]
}

22
examples/api/get.yml Normal file
View File

@@ -0,0 +1,22 @@
name: ""
request:
method: GET
url: /get
params:
foo1: bar1
foo2: bar2
headers:
Postman-Token: ea19464c-ddd4-4724-abe9-5e2b254c2723
validate:
- check: status_code
assert: equals
expect: 200
msg: assert response status code
- check: headers."Content-Type"
assert: equals
expect: application/json; charset=utf-8
msg: assert response header Content-Type
- check: body.url
assert: equals
expect: https://postman-echo.com/get?foo1=bar1&foo2=bar2
msg: assert response body url

45
examples/api/post.json Normal file
View File

@@ -0,0 +1,45 @@
{
"name": "",
"request": {
"method": "POST",
"url": "/post",
"headers": {
"Content-Length": "58",
"Content-Type": "text/plain",
"Postman-Token": "$session_token"
},
"body": "This is expected to be sent back as part of response body."
},
"validate": [
{
"check": "status_code",
"assert": "equals",
"expect": 200,
"msg": "assert response status code"
},
{
"check": "headers.\"Content-Type\"",
"assert": "equals",
"expect": "application/json; charset=utf-8",
"msg": "assert response header Content-Type"
},
{
"check": "body.data",
"assert": "equals",
"expect": "This is expected to be sent back as part of response body.",
"msg": "assert response body data"
},
{
"check": "body.json",
"assert": "equals",
"expect": null,
"msg": "assert response body json"
},
{
"check": "body.url",
"assert": "equals",
"expect": "https://postman-echo.com/post",
"msg": "assert response body url"
}
]
}

30
examples/api/post.yml Normal file
View File

@@ -0,0 +1,30 @@
name: ""
request:
method: POST
url: /post
headers:
Content-Length: "58"
Content-Type: text/plain
Postman-Token: $session_token
body: This is expected to be sent back as part of response body.
validate:
- check: status_code
assert: equals
expect: 200
msg: assert response status code
- check: headers."Content-Type"
assert: equals
expect: application/json; charset=utf-8
msg: assert response header Content-Type
- check: body.data
assert: equals
expect: This is expected to be sent back as part of response body.
msg: assert response body data
- check: body.json
assert: equals
expect: null
msg: assert response body json
- check: body.url
assert: equals
expect: https://postman-echo.com/post
msg: assert response body url

45
examples/api/put.json Normal file
View File

@@ -0,0 +1,45 @@
{
"name": "",
"request": {
"method": "PUT",
"url": "/put",
"headers": {
"Content-Length": "58",
"Content-Type": "text/plain",
"Postman-Token": "5d357b2b-0f10-4ded-bc9a-299ebef7a2d5"
},
"body": "This is expected to be sent back as part of response body."
},
"validate": [
{
"check": "status_code",
"assert": "equals",
"expect": 200,
"msg": "assert response status code"
},
{
"check": "headers.\"Content-Type\"",
"assert": "equals",
"expect": "application/json; charset=utf-8",
"msg": "assert response header Content-Type"
},
{
"check": "body.data",
"assert": "equals",
"expect": "This is expected to be sent back as part of response body.",
"msg": "assert response body data"
},
{
"check": "body.json",
"assert": "equals",
"expect": null,
"msg": "assert response body json"
},
{
"check": "body.url",
"assert": "equals",
"expect": "https://postman-echo.com/put",
"msg": "assert response body url"
}
]
}

30
examples/api/put.yml Normal file
View File

@@ -0,0 +1,30 @@
name: ""
request:
method: PUT
url: /put
headers:
Content-Length: "58"
Content-Type: text/plain
Postman-Token: 5d357b2b-0f10-4ded-bc9a-299ebef7a2d5
body: This is expected to be sent back as part of response body.
validate:
- check: status_code
assert: equals
expect: 200
msg: assert response status code
- check: headers."Content-Type"
assert: equals
expect: application/json; charset=utf-8
msg: assert response header Content-Type
- check: body.data
assert: equals
expect: This is expected to be sent back as part of response body.
msg: assert response body data
- check: body.json
assert: equals
expect: null
msg: assert response body json
- check: body.url
assert: equals
expect: https://postman-echo.com/put
msg: assert response body url

View File

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

View File

@@ -585,13 +585,13 @@
{ {
"check": "status_code", "check": "status_code",
"assert": "equals", "assert": "equals",
"expect": 302, "expect": 200,
"msg": "assert response status code" "msg": "assert response status code"
}, },
{ {
"check": "headers.\"Content-Type\"", "check": "headers.\"Content-Type\"",
"assert": "equals", "assert": "equals",
"expect": "text/plain; charset=utf-8", "expect": "application/json; charset=utf-8",
"msg": "assert response header Content-Type" "msg": "assert response header Content-Type"
} }
] ]
@@ -695,13 +695,13 @@
{ {
"check": "status_code", "check": "status_code",
"assert": "equals", "assert": "equals",
"expect": 302, "expect": 200,
"msg": "assert response status code" "msg": "assert response status code"
}, },
{ {
"check": "headers.\"Content-Type\"", "check": "headers.\"Content-Type\"",
"assert": "equals", "assert": "equals",
"expect": "text/plain; charset=utf-8", "expect": "application/json; charset=utf-8",
"msg": "assert response header Content-Type" "msg": "assert response header Content-Type"
} }
] ]

View File

@@ -411,11 +411,11 @@ teststeps:
validate: validate:
- check: status_code - check: status_code
assert: equals assert: equals
expect: 302 expect: 200
msg: assert response status code msg: assert response status code
- check: headers."Content-Type" - check: headers."Content-Type"
assert: equals assert: equals
expect: text/plain; charset=utf-8 expect: application/json; charset=utf-8
msg: assert response header Content-Type msg: assert response header Content-Type
- name: "" - name: ""
request: request:
@@ -490,11 +490,11 @@ teststeps:
validate: validate:
- check: status_code - check: status_code
assert: equals assert: equals
expect: 302 expect: 200
msg: assert response status code msg: assert response status code
- check: headers."Content-Type" - check: headers."Content-Type"
assert: equals assert: equals
expect: text/plain; charset=utf-8 expect: application/json; charset=utf-8
msg: assert response header Content-Type msg: assert response header Content-Type
- name: "" - name: ""
request: request:

View File

@@ -0,0 +1,78 @@
{
"config": {
"name": "api test demo",
"variables": {
"user_agent": "iOS/10.3",
"device_sn": "TESTCASE_SETUP_XXX",
"os_platform": "ios",
"app_version": "2.8.6"
},
"base_url": "https://postman-echo.com",
"herader": [
{
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate, br",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Host": "postman-echo.com",
"User-Agent": "PostmanRuntime/7.28.4"
}
],
"verify": false,
"export": [
"session_token"
]
},
"teststeps": [
{
"name": "test api /get",
"api": "examples/api/get.json",
"variables": {
"user_agent": "iOS/10.4",
"device_sn": "$device_sn",
"os_platform": "ios",
"app_version": "2.8.7"
},
"extract": {
"session_token": "body.headers.\"postman-token\""
}
},
{
"name": "test api /post",
"api": "examples/api/post.json",
"variables": {
"user_agent": "iOS/10.5",
"device_sn": "$device_sn",
"os_platform": "ios",
"app_version": "2.8.9"
},
"validate": [
{
"eq": [
"status_code",
200
]
},
{
"eq": [
"body.headers.postman-token",
"ea19464c-ddd4-4724-abe9-5e2b254c2723"
]
}
]
},
{
"name": "test api /put",
"api": "examples/api/put.json",
"variables": {
"user_agent": "iOS/10.6",
"device_sn": "$device_sn",
"os_platform": "ios",
"app_version": "2.8.10"
},
"extract": {
"session_token": "body.headers.\"postman-token\""
}
}
]
}

View File

@@ -0,0 +1,47 @@
config:
name: 'api test demo'
variables:
user_agent: iOS/10.3
device_sn: TESTCASE_SETUP_XXX
os_platform: ios
app_version: 2.8.6
base_url: 'https://postman-echo.com'
herader:
- Accept: '*/*'
Accept-Encoding: 'gzip, deflate, br'
Cache-Control: no-cache
Connection: keep-alive
Host: postman-echo.com
User-Agent: PostmanRuntime/7.28.4
verify: false
export:
- session_token
teststeps:
- name: 'test api /get'
api: examples/api/get.json
variables:
user_agent: iOS/10.4
device_sn: $device_sn
os_platform: ios
app_version: 2.8.7
extract:
session_token: 'body.headers."postman-token"'
- name: 'test api /post'
api: examples/api/post.json
variables:
user_agent: iOS/10.5
device_sn: $device_sn
os_platform: ios
app_version: 2.8.9
validate:
- { eq: [ status_code, 200 ] }
- { eq: [ body.headers.postman-token, ea19464c-ddd4-4724-abe9-5e2b254c2723 ] }
- name: 'test api /put'
api: examples/api/put.json
variables:
user_agent: iOS/10.6
device_sn: $device_sn
os_platform: ios
app_version: 2.8.10
extract:
session_token: 'body.headers."postman-token"'

View File

@@ -0,0 +1,18 @@
{
"config": {
"name": "reference testcase test",
"base_url": "https://postman-echo.com",
"variables": {
"os_platform": "ios"
}
},
"teststeps": [
{
"name": "run demo_httprunner.json",
"testcase": "examples/demo_httprunner.json",
"variables": {
"os_platform": "$os_platform"
}
}
]
}

View File

@@ -0,0 +1,11 @@
config:
name: "reference testcase test"
base_url: "https://postman-echo.com"
variables:
os_platform: 'ios'
teststeps:
- name: run demo_httprunner.yaml
testcase: examples/demo_httprunner.yaml
variables:
os_platform: $os_platform

View File

@@ -16,6 +16,9 @@ type Boomer struct {
memoryProfile string memoryProfile string
memoryProfileDuration time.Duration memoryProfileDuration time.Duration
disableKeepalive bool
disableCompression bool
} }
// NewStandaloneBoomer returns a new Boomer, which can run without master. // NewStandaloneBoomer returns a new Boomer, which can run without master.
@@ -52,6 +55,24 @@ func (b *Boomer) SetRateLimiter(maxRPS int64, requestIncreaseRate string) {
} }
} }
// SetDisableKeepAlive disable keep-alive for tcp
func (b *Boomer) SetDisableKeepAlive(disableKeepalive bool) {
b.disableKeepalive = disableKeepalive
}
// SetDisableCompression disable compression to prevent the Transport from requesting compression with an "Accept-Encoding: gzip"
func (b *Boomer) SetDisableCompression(disableCompression bool) {
b.disableCompression = disableCompression
}
func (b *Boomer) GetDisableKeepAlive() bool {
return b.disableKeepalive
}
func (b *Boomer) GetDisableCompression() bool {
return b.disableCompression
}
// SetLoopCount set loop count for test. // SetLoopCount set loop count for test.
func (b *Boomer) SetLoopCount(loopCount int64) { func (b *Boomer) SetLoopCount(loopCount int64) {
b.localRunner.loop = &Loop{loopCount: loopCount} b.localRunner.loop = &Loop{loopCount: loopCount}

View File

@@ -255,6 +255,10 @@ func deserializeStatsEntry(stat interface{}) (entryOutput *statsEntryOutput, err
var duration float64 var duration float64
if entry.Name == "Total" { if entry.Name == "Total" {
duration = float64(entry.LastRequestTimestamp - entry.StartTime) duration = float64(entry.LastRequestTimestamp - entry.StartTime)
// fix: avoid divide by zero
if duration < 1 {
duration = 1
}
} else { } else {
duration = float64(reportStatsInterval / time.Second) duration = float64(reportStatsInterval / time.Second)
} }

View File

@@ -211,3 +211,12 @@ func EnsureFolderExists(folderPath string) error {
} }
return nil return nil
} }
func Contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}

View File

@@ -12,4 +12,5 @@ var (
MarshalIndent = json.MarshalIndent MarshalIndent = json.MarshalIndent
Unmarshal = json.Unmarshal Unmarshal = json.Unmarshal
NewDecoder = json.NewDecoder NewDecoder = json.NewDecoder
Get = json.Get
) )

View File

@@ -12,8 +12,8 @@ import (
) )
var ( var (
demoTestCaseJSONPath = "../../examples/demo.json" demoTestCaseJSONPath hrp.TestCasePath = "../../examples/demo.json"
demoTestCaseYAMLPath = "../../examples/demo.yaml" demoTestCaseYAMLPath hrp.TestCasePath = "../../examples/demo.yaml"
) )
func buildHashicorpPlugin() { func buildHashicorpPlugin() {
@@ -33,11 +33,11 @@ func removeHashicorpPlugin() {
func TestGenDemoTestCase(t *testing.T) { func TestGenDemoTestCase(t *testing.T) {
tCase, _ := demoTestCase.ToTCase() tCase, _ := demoTestCase.ToTCase()
err := builtin.Dump2JSON(tCase, demoTestCaseJSONPath) err := builtin.Dump2JSON(tCase, demoTestCaseJSONPath.ToString())
if err != nil { if err != nil {
t.Fail() t.Fail()
} }
err = builtin.Dump2YAML(tCase, demoTestCaseYAMLPath) err = builtin.Dump2YAML(tCase, demoTestCaseYAMLPath.ToString())
if err != nil { if err != nil {
t.Fail() t.Fail()
} }
@@ -58,8 +58,7 @@ func TestJsonDemo(t *testing.T) {
buildHashicorpPlugin() buildHashicorpPlugin()
defer removeHashicorpPlugin() defer removeHashicorpPlugin()
testCase := &hrp.TestCasePath{Path: demoTestCaseJSONPath} err := hrp.NewRunner(nil).Run(&demoTestCaseJSONPath) // hrp.Run(testCase)
err := hrp.NewRunner(nil).Run(testCase) // hrp.Run(testCase)
if err != nil { if err != nil {
t.Fail() t.Fail()
} }
@@ -69,8 +68,7 @@ func TestYamlDemo(t *testing.T) {
buildHashicorpPlugin() buildHashicorpPlugin()
defer removeHashicorpPlugin() defer removeHashicorpPlugin()
testCase := &hrp.TestCasePath{Path: demoTestCaseYAMLPath} err := hrp.NewRunner(nil).Run(&demoTestCaseYAMLPath) // hrp.Run(testCase)
err := hrp.NewRunner(nil).Run(testCase) // hrp.Run(testCase)
if err != nil { if err != nil {
t.Fail() t.Fail()
} }

View File

@@ -26,6 +26,7 @@ type TConfig struct {
Name string `json:"name" yaml:"name"` // required Name string `json:"name" yaml:"name"` // required
Verify bool `json:"verify,omitempty" yaml:"verify,omitempty"` Verify bool `json:"verify,omitempty" yaml:"verify,omitempty"`
BaseURL string `json:"base_url,omitempty" yaml:"base_url,omitempty"` BaseURL string `json:"base_url,omitempty" yaml:"base_url,omitempty"`
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"` Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"` Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"`
ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"` ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"`
@@ -104,6 +105,21 @@ type Request struct {
Verify bool `json:"verify,omitempty" yaml:"verify,omitempty"` Verify bool `json:"verify,omitempty" yaml:"verify,omitempty"`
} }
type API struct {
Name string `json:"name" yaml:"name"` // required
Request *Request `json:"request,omitempty" yaml:"request,omitempty"`
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
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 []interface{} `json:"validate,omitempty" yaml:"validate,omitempty"`
Export []string `json:"export,omitempty" yaml:"export,omitempty"`
}
func (api *API) ToAPI() (*API, error) {
return api, nil
}
// Validator represents validator for one HTTP response. // Validator represents validator for one HTTP response.
type Validator struct { type Validator struct {
Check string `json:"check" yaml:"check"` // get value with jmespath Check string `json:"check" yaml:"check"` // get value with jmespath
@@ -112,20 +128,29 @@ type Validator struct {
Message string `json:"msg,omitempty" yaml:"msg,omitempty"` // optional Message string `json:"msg,omitempty" yaml:"msg,omitempty"` // optional
} }
// IAPI represents interface for api,
// includes API and APIPath.
type IAPI interface {
ToAPI() (*API, error)
}
// TStep represents teststep data structure. // TStep represents teststep data structure.
// Each step maybe two different type: make one HTTP request or reference another testcase. // Each step maybe two different type: make one HTTP request or reference another testcase.
type TStep struct { type TStep struct {
Name string `json:"name" yaml:"name"` // required Name string `json:"name" yaml:"name"` // required
Request *Request `json:"request,omitempty" yaml:"request,omitempty"` Request *Request `json:"request,omitempty" yaml:"request,omitempty"`
TestCase *TestCase `json:"testcase,omitempty" yaml:"testcase,omitempty"` APIPath string `json:"api,omitempty" yaml:"api,omitempty"`
Transaction *Transaction `json:"transaction,omitempty" yaml:"transaction,omitempty"` TestCasePath string `json:"testcase,omitempty" yaml:"testcase,omitempty"`
Rendezvous *Rendezvous `json:"rendezvous,omitempty" yaml:"rendezvous,omitempty"` APIContent IAPI `json:"api_content,omitempty" yaml:"api_content,omitempty"`
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"` TestCaseContent ITestCase `json:"testcase_content,omitempty" yaml:"testcase_content,omitempty"`
SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"` Transaction *Transaction `json:"transaction,omitempty" yaml:"transaction,omitempty"`
TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"` Rendezvous *Rendezvous `json:"rendezvous,omitempty" yaml:"rendezvous,omitempty"`
Extract map[string]string `json:"extract,omitempty" yaml:"extract,omitempty"` Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
Validators []interface{} `json:"validate,omitempty" yaml:"validate,omitempty"` SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"`
Export []string `json:"export,omitempty" yaml:"export,omitempty"` TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"`
Extract map[string]string `json:"extract,omitempty" yaml:"extract,omitempty"`
Validators []interface{} `json:"validate,omitempty" yaml:"validate,omitempty"`
Export []string `json:"export,omitempty" yaml:"export,omitempty"`
} }
type stepType string type stepType string

View File

@@ -281,6 +281,99 @@ func mergeVariables(variables, overriddenVariables map[string]interface{}) map[s
return mergedVariables return mergedVariables
} }
// merge two map, the first map have higher priority
func mergeMap(m, overriddenMap map[string]string) map[string]string {
if overriddenMap == nil {
return m
}
if m == nil {
return overriddenMap
}
mergedMap := make(map[string]string)
for k, v := range overriddenMap {
mergedMap[k] = v
}
for k, v := range m {
mergedMap[k] = v
}
return mergedMap
}
// merge two validators slice, the first validators have higher priority
func mergeValidators(validators, overriddenValidators []interface{}) []interface{} {
if validators == nil {
return overriddenValidators
}
if overriddenValidators == nil {
return validators
}
var mergedValidators []interface{}
validators = append(validators, overriddenValidators...)
for _, validator := range validators {
flag := true
for _, mergedValidator := range mergedValidators {
if validator.(Validator).Check == mergedValidator.(Validator).Check {
flag = false
break
}
}
if flag {
mergedValidators = append(mergedValidators, validator)
}
}
return mergedValidators
}
// merge two slices, the first slice have higher priority
func mergeSlices(slice, overriddenSlice []string) []string {
if slice == nil {
return overriddenSlice
}
if overriddenSlice == nil {
return slice
}
for _, value := range overriddenSlice {
if !builtin.Contains(slice, value) {
slice = append(slice, value)
}
}
return slice
}
// extend teststep with api, teststep will merge and override referenced api
func extendWithAPI(testStep *TStep, overriddenStep *API) {
// override api name
if testStep.Name == "" {
testStep.Name = overriddenStep.Name
}
// merge & override request
testStep.Request = overriddenStep.Request
// merge & override variables
testStep.Variables = mergeVariables(testStep.Variables, overriddenStep.Variables)
// merge & override extractors
testStep.Extract = mergeMap(testStep.Extract, overriddenStep.Extract)
// merge & override validators
testStep.Validators = mergeValidators(testStep.Validators, overriddenStep.Validators)
// merge & override setupHooks
testStep.SetupHooks = mergeSlices(testStep.SetupHooks, overriddenStep.SetupHooks)
// merge & override teardownHooks
testStep.TeardownHooks = mergeSlices(testStep.TeardownHooks, overriddenStep.TeardownHooks)
}
// extend referenced testcase with teststep, teststep config merge and override referenced testcase config
func extendWithTestCase(testStep *TStep, overriddenTestCase *TestCase) {
// override testcase name
if testStep.Name != "" {
overriddenTestCase.Config.Name = testStep.Name
}
// merge & override variables
overriddenTestCase.Config.Variables = mergeVariables(testStep.Variables, overriddenTestCase.Config.Variables)
// merge & override extractors
overriddenTestCase.Config.Export = mergeSlices(testStep.Export, overriddenTestCase.Config.Export)
}
var eval = goval.NewEvaluator() var eval = goval.NewEvaluator()
// literalEval parse string to number if possible // literalEval parse string to number if possible

View File

@@ -333,6 +333,112 @@ func TestMergeVariables(t *testing.T) {
} }
} }
func TestMergeMap(t *testing.T) {
testData := []struct {
m map[string]string
overriddenMap map[string]string
expectMap map[string]string
}{
{
map[string]string{"Accept": "*/*", "Accept-Encoding": "gzip, deflate, br", "Connection": "close"},
map[string]string{"Cache-Control": "no-cache", "Connection": "keep-alive"},
map[string]string{"Accept": "*/*", "Accept-Encoding": "gzip, deflate, br", "Connection": "close", "Cache-Control": "no-cache"},
},
{
map[string]string{"Host": "postman-echo.com", "Postman-Token": "ea19464c-ddd4-4724-abe9-5e2b254c2723"},
map[string]string{"Host": "Postman-echo.com", "Connection": "keep-alive", "Postman-Token": "ea19464c-ddd4-4724-abe9-5e2b342c2723"},
map[string]string{"Host": "postman-echo.com", "Postman-Token": "ea19464c-ddd4-4724-abe9-5e2b254c2723", "Connection": "keep-alive"},
},
{
map[string]string{"Accept": "*/*", "Accept-Encoding": "gzip, deflate, br", "Connection": "close"},
nil,
map[string]string{"Accept": "*/*", "Accept-Encoding": "gzip, deflate, br", "Connection": "close"},
},
{
nil,
map[string]string{"Cache-Control": "no-cache", "Connection": "keep-alive"},
map[string]string{"Cache-Control": "no-cache", "Connection": "keep-alive"},
},
}
for _, data := range testData {
mergedMap := mergeMap(data.m, data.overriddenMap)
if !assert.Equal(t, data.expectMap, mergedMap) {
t.Fail()
}
}
}
func TestMergeSlices(t *testing.T) {
testData := []struct {
slice []string
overriddenSlice []string
expectSlice []string
}{
{
[]string{"${setup_hook_example1($name)}", "${setup_hook_example2($name)}"},
[]string{"${setup_hook_example3($name)}", "${setup_hook_example4($name)}"},
[]string{"${setup_hook_example1($name)}", "${setup_hook_example2($name)}", "${setup_hook_example3($name)}", "${setup_hook_example4($name)}"},
},
{
[]string{"${setup_hook_example1($name)}", "${setup_hook_example2($name)}"},
nil,
[]string{"${setup_hook_example1($name)}", "${setup_hook_example2($name)}"},
},
{
nil,
[]string{"${setup_hook_example3($name)}", "${setup_hook_example4($name)}"},
[]string{"${setup_hook_example3($name)}", "${setup_hook_example4($name)}"},
},
}
for _, data := range testData {
mergedSlice := mergeSlices(data.slice, data.overriddenSlice)
if !assert.Equal(t, data.expectSlice, mergedSlice) {
t.Fail()
}
}
}
func TestMergeValidators(t *testing.T) {
testData := []struct {
validators []interface{}
overriddenValidators []interface{}
expectValidators []interface{}
}{
{
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 200, Message: "assert response status code"}},
[]interface{}{Validator{Check: `headers."Content-Type"`, Assert: "equals", Expect: "application/json; charset=utf-8", Message: "assert response header Content-Typ"}},
[]interface{}{
Validator{Check: "status_code", Assert: "equals", Expect: 200, Message: "assert response status code"},
Validator{Check: `headers."Content-Type"`, Assert: "equals", Expect: "application/json; charset=utf-8", Message: "assert response header Content-Typ"},
},
},
{
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 302, Message: "assert response status code"}},
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 200, Message: "assert response status code"}},
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 302, Message: "assert response status code"}},
},
{
nil,
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 200, Message: "assert response status code"}},
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 200, Message: "assert response status code"}},
},
{
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 302, Message: "assert response status code"}},
nil,
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 302, Message: "assert response status code"}},
},
}
for _, data := range testData {
mergedValidators := mergeValidators(data.validators, data.overriddenValidators)
if !assert.Equal(t, data.expectValidators, mergedValidators) {
t.Fail()
}
}
}
func TestCallBuiltinFunction(t *testing.T) { func TestCallBuiltinFunction(t *testing.T) {
parser := newParser() parser := newParser()

View File

@@ -3,13 +3,14 @@ package hrp
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"compress/flate"
"compress/gzip" "compress/gzip"
"compress/zlib"
"crypto/tls" "crypto/tls"
_ "embed" _ "embed"
"fmt" "fmt"
"html/template" "html/template"
"io" "io"
"net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
@@ -74,6 +75,20 @@ type HRPRunner struct {
client *http.Client client *http.Client
} }
// SetClientTransport configures transport of http client for high concurrency load testing
func (r *HRPRunner) SetClientTransport(maxConns int, disableKeepAlive bool, disableCompression bool) *HRPRunner {
log.Info().Int("maxConns", maxConns).Msg("[init] SetClientTransport")
r.client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
DialContext: (&net.Dialer{}).DialContext,
MaxIdleConns: 0,
MaxIdleConnsPerHost: maxConns,
DisableKeepAlives: disableKeepAlive,
DisableCompression: disableCompression,
}
return r
}
// SetFailfast configures whether to stop running when one step fails. // SetFailfast configures whether to stop running when one step fails.
func (r *HRPRunner) SetFailfast(failfast bool) *HRPRunner { func (r *HRPRunner) SetFailfast(failfast bool) *HRPRunner {
log.Info().Bool("failfast", failfast).Msg("[init] SetFailfast") log.Info().Bool("failfast", failfast).Msg("[init] SetFailfast")
@@ -356,12 +371,21 @@ func (r *caseRunner) runStep(index int, caseConfig *TConfig) (stepResult *stepDa
if _, ok := step.(*StepTestCaseWithOptionalArgs); ok { if _, ok := step.(*StepTestCaseWithOptionalArgs); ok {
// run referenced testcase // run referenced testcase
log.Info().Str("testcase", copiedStep.Name).Msg("run referenced testcase") log.Info().Str("testcase", copiedStep.Name).Msg("run referenced testcase")
// TODO: override testcase config
stepResult, err = r.runStepTestCase(copiedStep) stepResult, err = r.runStepTestCase(copiedStep)
if err != nil { if err != nil {
log.Error().Err(err).Msg("run referenced testcase step failed") log.Error().Err(err).Msg("run referenced testcase step failed")
} }
} else { } else {
if _, ok := step.(*StepAPIWithOptionalArgs); ok {
// run referenced API
log.Info().Str("api", copiedStep.Name).Msg("run referenced api")
api, _ := copiedStep.APIContent.ToAPI()
extendWithAPI(copiedStep, api)
}
// override headers
if caseConfig.Headers != nil {
copiedStep.Request.Headers = mergeMap(copiedStep.Request.Headers, caseConfig.Headers)
}
// parse step request url // parse step request url
var requestUrl interface{} var requestUrl interface{}
requestUrl, err = r.parser.parseString(copiedStep.Request.URL, copiedStep.Variables) requestUrl, err = r.parser.parseString(copiedStep.Request.URL, copiedStep.Variables)
@@ -634,7 +658,6 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro
Proto: "HTTP/1.1", Proto: "HTTP/1.1",
ProtoMajor: 1, ProtoMajor: 1,
ProtoMinor: 1, ProtoMinor: 1,
Close: true, // prevent the connection from being re-used
} }
// prepare request headers // prepare request headers
@@ -906,19 +929,22 @@ func shouldPrintBody(contentType string) bool {
return false return false
} }
func decodeResponseBody(resp *http.Response) error { func decodeResponseBody(resp *http.Response) (err error) {
switch resp.Header.Get("Content-Encoding") { switch resp.Header.Get("Content-Encoding") {
case "br": case "br":
resp.Body = io.NopCloser(brotli.NewReader(resp.Body)) resp.Body = io.NopCloser(brotli.NewReader(resp.Body))
case "gzip": case "gzip":
gr, err := gzip.NewReader(resp.Body) resp.Body, err = gzip.NewReader(resp.Body)
if err != nil { if err != nil {
return err return err
} }
resp.Body = gr
resp.ContentLength = -1 // set to unknown to avoid Content-Length mismatched resp.ContentLength = -1 // set to unknown to avoid Content-Length mismatched
case "deflate": case "deflate":
resp.Body = flate.NewReader(resp.Body) resp.Body, err = zlib.NewReader(resp.Body)
if err != nil {
return err
}
resp.ContentLength = -1 // set to unknown to avoid Content-Length mismatched
} }
return nil return nil
} }
@@ -929,7 +955,7 @@ func (r *caseRunner) runStepTestCase(step *TStep) (stepResult *stepData, err err
StepType: stepTypeTestCase, StepType: stepTypeTestCase,
Success: false, Success: false,
} }
testcase := step.TestCase testcase := step.TestCaseContent
// copy testcase to avoid data racing // copy testcase to avoid data racing
copiedTestCase := &TestCase{} copiedTestCase := &TestCase{}
@@ -937,6 +963,8 @@ func (r *caseRunner) runStepTestCase(step *TStep) (stepResult *stepData, err err
log.Error().Err(err).Msg("copy testcase failed") log.Error().Err(err).Msg("copy testcase failed")
return stepResult, err return stepResult, err
} }
// override testcase config
extendWithTestCase(step, copiedTestCase)
start := time.Now() start := time.Now()
caseRunnerObj := r.hrpRunner.newCaseRunner(copiedTestCase) caseRunnerObj := r.hrpRunner.newCaseRunner(copiedTestCase)
@@ -946,6 +974,8 @@ func (r *caseRunner) runStepTestCase(step *TStep) (stepResult *stepData, err err
return stepResult, err return stepResult, err
} }
stepResult.Data = caseRunnerObj.getSummary() stepResult.Data = caseRunnerObj.getSummary()
// export testcase export variables
stepResult.ExportVars = caseRunnerObj.summary.InOut.ExportVars
stepResult.Success = true stepResult.Success = true
return stepResult, nil return stepResult, nil
} }
@@ -991,7 +1021,7 @@ func (r *caseRunner) getSummary() *testCaseSummary {
caseSummary.Time.Duration = time.Since(r.startTime).Seconds() caseSummary.Time.Duration = time.Since(r.startTime).Seconds()
exportVars := make(map[string]interface{}) exportVars := make(map[string]interface{})
for _, value := range r.Config.Export { for _, value := range r.Config.Export {
exportVars[value] = r.Config.Variables[value] exportVars[value] = r.sessionVariables[value]
} }
caseSummary.InOut.ExportVars = exportVars caseSummary.InOut.ExportVars = exportVars
caseSummary.InOut.ConfigVars = r.Config.Variables caseSummary.InOut.ConfigVars = r.Config.Variables

View File

@@ -49,17 +49,26 @@ func TestHttpRunner(t *testing.T) {
AssertEqual("status_code", 200, "check status code"). AssertEqual("status_code", 200, "check status code").
AssertEqual("headers.\"Content-Type\"", "application/json", "check http response Content-Type"), AssertEqual("headers.\"Content-Type\"", "application/json", "check http response Content-Type"),
}}), }}),
NewStep("TestCase4").CallRefCase(&demoRefAPIYAMLPath),
NewStep("TestCase5").CallRefCase(&demoTestCaseJSONPath),
}, },
} }
testcase2 := &TestCase{ testcase2 := &TestCase{
Config: NewConfig("TestCase2").SetWeight(3), Config: NewConfig("TestCase2").SetWeight(3),
} }
testcase3 := &TestCasePath{demoTestCaseJSONPath} testcase3 := &TestCase{
Config: NewConfig("TestCase1").
SetBaseURL("https://postman-echo.com"),
TestSteps: []IStep{
NewStep("TestCase5").CallRefAPI(&demoAPIYAMLPath),
},
}
testcase4 := &demoRefTestCaseJSONPath
r := NewRunner(t) r := NewRunner(t)
r.saveTests = true r.saveTests = true
r.genHTMLReport = true r.genHTMLReport = true
err := r.Run(testcase1, testcase2, testcase3) err := r.Run(testcase1, testcase2, testcase3, testcase4)
if err != nil { if err != nil {
t.Fatalf("run testcase error: %v", err) t.Fatalf("run testcase error: %v", err)
} }

55
step.go
View File

@@ -22,6 +22,12 @@ func (c *TConfig) SetBaseURL(baseURL string) *TConfig {
return c return c
} }
// SetHeaders sets global headers for current testcase.
func (c *TConfig) SetHeaders(headers map[string]string) *TConfig {
c.Headers = headers
return c
}
// SetVerifySSL sets whether to verify SSL for current testcase. // SetVerifySSL sets whether to verify SSL for current testcase.
func (c *TConfig) SetVerifySSL(verify bool) *TConfig { func (c *TConfig) SetVerifySSL(verify bool) *TConfig {
c.Verify = verify c.Verify = verify
@@ -150,13 +156,21 @@ func (s *StepRequest) PATCH(url string) *StepRequestWithOptionalArgs {
} }
// CallRefCase calls a referenced testcase. // CallRefCase calls a referenced testcase.
func (s *StepRequest) CallRefCase(tc *TestCase) *StepTestCaseWithOptionalArgs { func (s *StepRequest) CallRefCase(tc ITestCase) *StepTestCaseWithOptionalArgs {
s.step.TestCase = tc s.step.TestCaseContent, _ = tc.ToTestCase()
return &StepTestCaseWithOptionalArgs{ return &StepTestCaseWithOptionalArgs{
step: s.step, step: s.step,
} }
} }
// CallRefAPI calls a referenced api.
func (s *StepRequest) CallRefAPI(api IAPI) *StepAPIWithOptionalArgs {
s.step.APIContent, _ = api.ToAPI()
return &StepAPIWithOptionalArgs{
step: s.step,
}
}
// StartTransaction starts a transaction. // StartTransaction starts a transaction.
func (s *StepRequest) StartTransaction(name string) *StepTransaction { func (s *StepRequest) StartTransaction(name string) *StepTransaction {
s.step.Transaction = &Transaction{ s.step.Transaction = &Transaction{
@@ -274,6 +288,40 @@ func (s *StepRequestWithOptionalArgs) ToStruct() *TStep {
return s.step return s.step
} }
// StepAPIWithOptionalArgs implements IStep interface.
type StepAPIWithOptionalArgs struct {
step *TStep
}
// TeardownHook adds a teardown hook for current teststep.
func (s *StepAPIWithOptionalArgs) TeardownHook(hook string) *StepAPIWithOptionalArgs {
s.step.TeardownHooks = append(s.step.TeardownHooks, hook)
return s
}
// Export specifies variable names to export from referenced api for current step.
func (s *StepAPIWithOptionalArgs) Export(names ...string) *StepAPIWithOptionalArgs {
api, _ := s.step.APIContent.ToAPI()
s.step.Export = append(api.Export, names...)
return s
}
func (s *StepAPIWithOptionalArgs) Name() string {
if s.step.Name != "" {
return s.step.Name
}
api, _ := s.step.APIContent.ToAPI()
return api.Name
}
func (s *StepAPIWithOptionalArgs) Type() string {
return "api"
}
func (s *StepAPIWithOptionalArgs) ToStruct() *TStep {
return s.step
}
// StepTestCaseWithOptionalArgs implements IStep interface. // StepTestCaseWithOptionalArgs implements IStep interface.
type StepTestCaseWithOptionalArgs struct { type StepTestCaseWithOptionalArgs struct {
step *TStep step *TStep
@@ -295,7 +343,8 @@ func (s *StepTestCaseWithOptionalArgs) Name() string {
if s.step.Name != "" { if s.step.Name != "" {
return s.step.Name return s.step.Name
} }
return s.step.TestCase.Config.Name ts, _ := s.step.TestCaseContent.ToTestCase()
return ts.Config.Name
} }
func (s *StepTestCaseWithOptionalArgs) Type() string { func (s *StepTestCaseWithOptionalArgs) Type() string {

View File

@@ -16,13 +16,13 @@ var (
AssertEqual("body.args.foo1", "bar1", "check param foo1"). AssertEqual("body.args.foo1", "bar1", "check param foo1").
AssertEqual("body.args.foo2", "bar2", "check param foo2") AssertEqual("body.args.foo2", "bar2", "check param foo2")
stepPOSTData = NewStep("post form data"). stepPOSTData = NewStep("post form data").
POST("/post"). POST("/post").
WithParams(map[string]interface{}{"foo1": "bar1", "foo2": "bar2"}). WithParams(map[string]interface{}{"foo1": "bar1", "foo2": "bar2"}).
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus", "Content-Type": "application/x-www-form-urlencoded"}). WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus", "Content-Type": "application/x-www-form-urlencoded"}).
WithBody("a=1&b=2"). WithBody("a=1&b=2").
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")
) )
func TestRunRequestGetToStruct(t *testing.T) { func TestRunRequestGetToStruct(t *testing.T) {