mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-12 02:21:29 +08:00
feat: support think time for load testing #120
This commit is contained in:
@@ -150,6 +150,10 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend
|
||||
}
|
||||
} else if stepData.StepType == stepTypeRendezvous {
|
||||
// rendezvous
|
||||
// TODO: implement rendezvous in boomer
|
||||
} else if stepData.StepType == stepTypeThinkTime {
|
||||
// think time
|
||||
// no record required
|
||||
} else {
|
||||
// request or testcase step
|
||||
b.RecordSuccess(step.Type(), step.Name(), stepData.Elapsed, stepData.ContentSize)
|
||||
|
||||
@@ -156,6 +156,10 @@ func (tc *TCase) ToTestCase() (*TestCase, error) {
|
||||
testCase.TestSteps = append(testCase.TestSteps, &StepTestCaseWithOptionalArgs{
|
||||
step: step,
|
||||
})
|
||||
} else if step.ThinkTime != nil {
|
||||
testCase.TestSteps = append(testCase.TestSteps, &StepThinkTime{
|
||||
step: step,
|
||||
})
|
||||
} else if step.Request != nil {
|
||||
testCase.TestSteps = append(testCase.TestSteps, &StepRequestWithOptionalArgs{
|
||||
step: step,
|
||||
|
||||
@@ -11,6 +11,7 @@ var (
|
||||
demoTestCaseYAMLPath TestCasePath = "examples/demo.yaml"
|
||||
demoRefAPIYAMLPath TestCasePath = "examples/ref_api_test.yaml"
|
||||
demoRefTestCaseJSONPath TestCasePath = "examples/ref_testcase_test.json"
|
||||
demoThinkTimeJsonPath TestCasePath = "examples/think_time_test.json"
|
||||
demoAPIYAMLPath APIPath = "examples/api/put.yml"
|
||||
)
|
||||
|
||||
|
||||
63
examples/think_time_test.json
Normal file
63
examples/think_time_test.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"config": {
|
||||
"name": "think time test demo",
|
||||
"variables": {
|
||||
"app_version": "v1",
|
||||
"user_agent": "iOS/10.3"
|
||||
},
|
||||
"base_url": "https://postman-echo.com",
|
||||
"think_time": {
|
||||
"strategy": "random_percentage",
|
||||
"setting": {
|
||||
"min_percentage": 1,
|
||||
"max_percentage": 1.5
|
||||
},
|
||||
"limit": 4
|
||||
},
|
||||
"verify": false
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "get with params",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "/get",
|
||||
"headers": {
|
||||
"User-Agent": "$user_agent,$app_version"
|
||||
}
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "check status code"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "think time 1",
|
||||
"think_time": {
|
||||
"time": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "post with params",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "/post",
|
||||
"headers": {
|
||||
"User-Agent": "$user_agent,$app_version"
|
||||
}
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "check status code"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
40
examples/think_time_test.yaml
Normal file
40
examples/think_time_test.yaml
Normal file
@@ -0,0 +1,40 @@
|
||||
config:
|
||||
name: "think time test demo"
|
||||
variables:
|
||||
app_version: v1
|
||||
user_agent: iOS/10.3
|
||||
base_url: "https://postman-echo.com"
|
||||
think_time:
|
||||
strategy: random_percentage
|
||||
setting:
|
||||
min_percentage: 1.0
|
||||
max_percentage: 1.5
|
||||
limit: 4
|
||||
verify: False
|
||||
|
||||
teststeps:
|
||||
- name: get with params
|
||||
request:
|
||||
method: GET
|
||||
url: /get
|
||||
headers:
|
||||
User-Agent: $user_agent,$app_version
|
||||
validate:
|
||||
- check: status_code
|
||||
assert: equals
|
||||
expect: 200
|
||||
msg: check status code
|
||||
- name: think time 1
|
||||
think_time:
|
||||
time: 3
|
||||
- name: post with params
|
||||
request:
|
||||
method: POST
|
||||
url: /post
|
||||
headers:
|
||||
User-Agent: $user_agent,$app_version
|
||||
validate:
|
||||
- check: status_code
|
||||
assert: equals
|
||||
expect: 200
|
||||
msg: check status code
|
||||
@@ -5,12 +5,15 @@ import (
|
||||
"crypto/md5"
|
||||
"encoding/csv"
|
||||
"encoding/hex"
|
||||
builtinJSON "encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -220,3 +223,38 @@ func Contains(s []string, e string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetRandomNumber(min, max int) int {
|
||||
if min > max {
|
||||
return 0
|
||||
}
|
||||
r := rand.Intn(max - min + 1)
|
||||
return min + r
|
||||
}
|
||||
|
||||
func Interface2Float64(i interface{}) (float64, error) {
|
||||
switch i.(type) {
|
||||
case int:
|
||||
return float64(i.(int)), nil
|
||||
case int32:
|
||||
return float64(i.(int32)), nil
|
||||
case int64:
|
||||
return float64(i.(int64)), nil
|
||||
case float32:
|
||||
return float64(i.(float32)), nil
|
||||
case float64:
|
||||
return i.(float64), nil
|
||||
case string:
|
||||
intVar, err := strconv.Atoi(i.(string))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return float64(intVar), err
|
||||
}
|
||||
// json.Number
|
||||
value, ok := i.(builtinJSON.Number)
|
||||
if ok {
|
||||
return value.Float64()
|
||||
}
|
||||
return 0, errors.New("failed to convert interface to float64")
|
||||
}
|
||||
|
||||
87
models.go
87
models.go
@@ -3,10 +3,12 @@ package hrp
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/hrp/internal/builtin"
|
||||
"github.com/httprunner/hrp/internal/version"
|
||||
)
|
||||
|
||||
@@ -30,13 +32,14 @@ type TConfig struct {
|
||||
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
|
||||
Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"`
|
||||
ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"`
|
||||
ThinkTime *ThinkTimeConfig `json:"think_time,omitempty" yaml:"think_time,omitempty"`
|
||||
Export []string `json:"export,omitempty" yaml:"export,omitempty"`
|
||||
Weight int `json:"weight,omitempty" yaml:"weight,omitempty"`
|
||||
Path string `json:"path,omitempty" yaml:"path,omitempty"` // testcase file path
|
||||
}
|
||||
|
||||
type TParamsConfig struct {
|
||||
Strategy interface{} `json:"strategy,omitempty" yaml:"strategy,omitempty"`
|
||||
Strategy interface{} `json:"strategy,omitempty" yaml:"strategy,omitempty"` // map[string]string、string
|
||||
Iteration int `json:"iteration,omitempty" yaml:"iteration,omitempty"`
|
||||
Iterators []*Iterator `json:"parameterIterator,omitempty" yaml:"parameterIterator,omitempty"` // 保存参数的迭代器
|
||||
}
|
||||
@@ -46,6 +49,82 @@ const (
|
||||
strategySequential string = "Sequential"
|
||||
)
|
||||
|
||||
type ThinkTimeConfig struct {
|
||||
Strategy string `json:"strategy,omitempty" yaml:"strategy,omitempty"` // default、random、limit、multiply、ignore
|
||||
Setting interface{} `json:"setting,omitempty" yaml:"setting,omitempty"` // random(map): {"min_percentage": 0.5, "max_percentage": 1.5}; 10、multiply(float64): 1.5
|
||||
Limit float64 `json:"limit,omitempty" yaml:"limit,omitempty"` // limit think time no more than specific time, ignore if value <= 0
|
||||
}
|
||||
|
||||
const (
|
||||
thinkTimeDefault string = "default" // as recorded
|
||||
thinkTimeRandomPercentage string = "random_percentage" // use random percentage of recorded think time
|
||||
thinkTimeMultiply string = "multiply" // multiply recorded think time
|
||||
thinkTimeIgnore string = "ignore" // ignore recorded think time
|
||||
)
|
||||
|
||||
const (
|
||||
thinkTimeDefaultMultiply = 1
|
||||
)
|
||||
|
||||
var (
|
||||
thinkTimeDefaultRandom = map[string]float64{"min_percentage": 0.5, "max_percentage": 1.5}
|
||||
)
|
||||
|
||||
func (ttc *ThinkTimeConfig) checkThinkTime() {
|
||||
if ttc == nil {
|
||||
return
|
||||
}
|
||||
// unset strategy, set default strategy
|
||||
if ttc.Strategy == "" {
|
||||
ttc.Strategy = thinkTimeDefault
|
||||
}
|
||||
// check think time
|
||||
if ttc.Strategy == thinkTimeRandomPercentage {
|
||||
if ttc.Setting == nil || reflect.TypeOf(ttc.Setting).Kind() != reflect.Map {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
value, ok := ttc.Setting.(map[string]interface{})
|
||||
if !ok {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
if _, ok := value["min_percentage"]; !ok {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
if _, ok := value["max_percentage"]; !ok {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
left, err := builtin.Interface2Float64(value["min_percentage"])
|
||||
if err != nil {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
right, err := builtin.Interface2Float64(value["max_percentage"])
|
||||
if err != nil {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
ttc.Setting = map[string]float64{"min_percentage": left, "max_percentage": right}
|
||||
} else if ttc.Strategy == thinkTimeMultiply {
|
||||
if ttc.Setting == nil {
|
||||
ttc.Setting = float64(0) // default
|
||||
return
|
||||
}
|
||||
value, err := builtin.Interface2Float64(ttc.Setting)
|
||||
if err != nil {
|
||||
ttc.Setting = float64(0) // default
|
||||
return
|
||||
}
|
||||
ttc.Setting = value
|
||||
} else if ttc.Strategy != thinkTimeIgnore {
|
||||
// unrecognized strategy, set default strategy
|
||||
ttc.Strategy = thinkTimeDefault
|
||||
}
|
||||
}
|
||||
|
||||
type paramsType []map[string]interface{}
|
||||
|
||||
type Iterator struct {
|
||||
@@ -145,6 +224,7 @@ type TStep struct {
|
||||
TestCaseContent ITestCase `json:"testcase_content,omitempty" yaml:"testcase_content,omitempty"`
|
||||
Transaction *Transaction `json:"transaction,omitempty" yaml:"transaction,omitempty"`
|
||||
Rendezvous *Rendezvous `json:"rendezvous,omitempty" yaml:"rendezvous,omitempty"`
|
||||
ThinkTime *ThinkTime `json:"think_time,omitempty" yaml:"think_time,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"`
|
||||
@@ -160,8 +240,13 @@ const (
|
||||
stepTypeTestCase stepType = "testcase"
|
||||
stepTypeTransaction stepType = "transaction"
|
||||
stepTypeRendezvous stepType = "rendezvous"
|
||||
stepTypeThinkTime stepType = "thinktime"
|
||||
)
|
||||
|
||||
type ThinkTime struct {
|
||||
Time float64 `json:"time" yaml:"time"`
|
||||
}
|
||||
|
||||
type transactionType string
|
||||
|
||||
const (
|
||||
|
||||
@@ -41,7 +41,6 @@ func initPlugin(path string, logOn bool) (plugin funplugin.IPlugin, err error) {
|
||||
go func() {
|
||||
<-c
|
||||
plugin.Quit()
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
// report event for initializing plugin
|
||||
|
||||
54
runner.go
54
runner.go
@@ -297,13 +297,16 @@ func (r *caseRunner) run() error {
|
||||
func (r *caseRunner) runStep(index int, caseConfig *TConfig) (stepResult *stepData, err error) {
|
||||
step := r.TestCase.TestSteps[index]
|
||||
|
||||
// step type priority order: transaction > rendezvous > testcase > request
|
||||
// step type priority order: transaction > rendezvous > thinktime > testcase > request
|
||||
if stepTran, ok := step.(*StepTransaction); ok {
|
||||
// transaction step
|
||||
return r.runStepTransaction(stepTran.step.Transaction)
|
||||
} else if stepRend, ok := step.(*StepRendezvous); ok {
|
||||
// rendezvous step
|
||||
return r.runStepRendezvous(stepRend.step.Rendezvous)
|
||||
} else if stepThink, ok := step.(*StepThinkTime); ok {
|
||||
// think time step
|
||||
return r.runStepThinkTime(stepThink.step, caseConfig.ThinkTime)
|
||||
}
|
||||
|
||||
log.Info().Str("step", step.Name()).Msg("run step start")
|
||||
@@ -377,6 +380,52 @@ func (r *caseRunner) runStep(index int, caseConfig *TConfig) (stepResult *stepDa
|
||||
return stepResult, err
|
||||
}
|
||||
|
||||
func (r *caseRunner) runStepThinkTime(step *TStep, ttc *ThinkTimeConfig) (stepResult *stepData, err error) {
|
||||
thinkTime := step.ThinkTime
|
||||
log.Info().
|
||||
Str("name", step.Name).
|
||||
Float64("time", thinkTime.Time).
|
||||
Msg("think time")
|
||||
stepResult = &stepData{
|
||||
Name: step.Name,
|
||||
StepType: stepTypeThinkTime,
|
||||
Success: true,
|
||||
}
|
||||
if ttc == nil {
|
||||
ttc = &ThinkTimeConfig{thinkTimeDefault, nil, 0}
|
||||
}
|
||||
var tt time.Duration
|
||||
switch ttc.Strategy {
|
||||
case thinkTimeDefault:
|
||||
tt = time.Duration(thinkTime.Time*1000) * time.Millisecond
|
||||
case thinkTimeRandomPercentage:
|
||||
m, ok := ttc.Setting.(map[string]float64) // e.g. {"min_percentage": 0.5, "max_percentage": 1.5}
|
||||
if !ok {
|
||||
tt = time.Duration(thinkTime.Time*1000) * time.Millisecond
|
||||
break
|
||||
}
|
||||
res := builtin.GetRandomNumber(int(thinkTime.Time*m["min_percentage"]*1000), int(thinkTime.Time*m["max_percentage"]*1000))
|
||||
tt = time.Duration(res) * time.Millisecond
|
||||
case thinkTimeMultiply:
|
||||
value, ok := ttc.Setting.(float64) // e.g. 0.5
|
||||
if !ok || value <= 0 {
|
||||
value = thinkTimeDefaultMultiply
|
||||
}
|
||||
tt = time.Duration(thinkTime.Time*value*1000) * time.Millisecond
|
||||
case thinkTimeIgnore:
|
||||
// nothing to do
|
||||
}
|
||||
// no more than limit
|
||||
if ttc.Limit > 0 {
|
||||
limit := time.Duration(ttc.Limit*1000) * time.Millisecond
|
||||
if limit < tt {
|
||||
tt = limit
|
||||
}
|
||||
}
|
||||
time.Sleep(tt)
|
||||
return stepResult, nil
|
||||
}
|
||||
|
||||
func (r *caseRunner) runStepTransaction(transaction *Transaction) (stepResult *stepData, err error) {
|
||||
log.Info().
|
||||
Str("name", transaction.Name).
|
||||
@@ -966,6 +1015,9 @@ func (r *caseRunner) parseConfig(cfg *TConfig) error {
|
||||
}
|
||||
cfg.BaseURL = convertString(parsedBaseURL)
|
||||
|
||||
// ensure correction of think time config
|
||||
cfg.ThinkTime.checkThinkTime()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
@@ -146,6 +147,63 @@ func TestInitRendezvous(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestThinkTime(t *testing.T) {
|
||||
buildHashicorpPlugin()
|
||||
defer removeHashicorpPlugin()
|
||||
|
||||
testcases := []*TestCase{
|
||||
{
|
||||
Config: NewConfig("TestCase1"),
|
||||
TestSteps: []IStep{
|
||||
NewStep("thinkTime").SetThinkTime(2),
|
||||
},
|
||||
},
|
||||
{
|
||||
Config: NewConfig("TestCase2").
|
||||
SetThinkTime(thinkTimeIgnore, nil, 0),
|
||||
TestSteps: []IStep{
|
||||
NewStep("thinkTime").SetThinkTime(0.5),
|
||||
},
|
||||
},
|
||||
{
|
||||
Config: NewConfig("TestCase3").
|
||||
SetThinkTime(thinkTimeRandomPercentage, nil, 0),
|
||||
TestSteps: []IStep{
|
||||
NewStep("thinkTime").SetThinkTime(1),
|
||||
},
|
||||
},
|
||||
{
|
||||
Config: NewConfig("TestCase4").
|
||||
SetThinkTime(thinkTimeRandomPercentage, map[string]interface{}{"min_percentage": 2, "max_percentage": 3}, 2.5),
|
||||
TestSteps: []IStep{
|
||||
NewStep("thinkTime").SetThinkTime(1),
|
||||
},
|
||||
},
|
||||
{
|
||||
Config: NewConfig("TestCase5"),
|
||||
TestSteps: []IStep{
|
||||
NewStep("thinkTime").CallRefCase(&demoThinkTimeJsonPath), // think time: 3s, random pct: {"min_percentage":1, "max_percentage":1.5}, limit: 4s
|
||||
},
|
||||
},
|
||||
}
|
||||
expectedMinValue := []float64{2, 0, 0.5, 2, 3}
|
||||
expectedMaxValue := []float64{2.5, 0.5, 2, 3, 10}
|
||||
for idx, testcase := range testcases {
|
||||
r := NewRunner(t)
|
||||
startTime := time.Now()
|
||||
err := r.Run(testcase)
|
||||
if err != nil {
|
||||
t.Fatalf("run testcase error: %v", err)
|
||||
}
|
||||
duration := time.Since(startTime)
|
||||
minValue := time.Duration(expectedMinValue[idx]*1000) * time.Millisecond
|
||||
maxValue := time.Duration(expectedMaxValue[idx]*1000) * time.Millisecond
|
||||
if duration < minValue || duration > maxValue {
|
||||
t.Fatalf("failed to test think time, expect value: [%v, %v], actual value: %v", minValue, maxValue, duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenHTMLReport(t *testing.T) {
|
||||
summary := newOutSummary()
|
||||
caseSummary1 := newSummary()
|
||||
|
||||
33
step.go
33
step.go
@@ -40,6 +40,12 @@ func (c *TConfig) WithParameters(parameters map[string]interface{}) *TConfig {
|
||||
return c
|
||||
}
|
||||
|
||||
// SetThinkTime sets think time config for current testcase.
|
||||
func (c *TConfig) SetThinkTime(strategy string, cfg interface{}, limit float64) *TConfig {
|
||||
c.ThinkTime = &ThinkTimeConfig{strategy, cfg, limit}
|
||||
return c
|
||||
}
|
||||
|
||||
// ExportVars specifies variable names to export for current testcase.
|
||||
func (c *TConfig) ExportVars(vars ...string) *TConfig {
|
||||
c.Export = vars
|
||||
@@ -193,6 +199,16 @@ func (s *StepRequest) EndTransaction(name string) *StepTransaction {
|
||||
}
|
||||
}
|
||||
|
||||
// SetThinkTime sets think time.
|
||||
func (s *StepRequest) SetThinkTime(time float64) *StepThinkTime {
|
||||
s.step.ThinkTime = &ThinkTime{
|
||||
Time: time,
|
||||
}
|
||||
return &StepThinkTime{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// StepRequestWithOptionalArgs implements IStep interface.
|
||||
type StepRequestWithOptionalArgs struct {
|
||||
step *TStep
|
||||
@@ -355,6 +371,23 @@ func (s *StepTestCaseWithOptionalArgs) ToStruct() *TStep {
|
||||
return s.step
|
||||
}
|
||||
|
||||
// StepThinkTime implements IStep interface.
|
||||
type StepThinkTime struct {
|
||||
step *TStep
|
||||
}
|
||||
|
||||
func (s *StepThinkTime) Name() string {
|
||||
return s.step.Name
|
||||
}
|
||||
|
||||
func (s *StepThinkTime) Type() string {
|
||||
return "thinktime"
|
||||
}
|
||||
|
||||
func (s *StepThinkTime) ToStruct() *TStep {
|
||||
return s.step
|
||||
}
|
||||
|
||||
// StepTransaction implements IStep interface.
|
||||
type StepTransaction struct {
|
||||
step *TStep
|
||||
|
||||
Reference in New Issue
Block a user