pull main

This commit is contained in:
debugtalk
2022-01-05 20:49:02 +08:00
16 changed files with 954 additions and 35 deletions

View File

@@ -3,6 +3,7 @@ package hrp
import (
"time"
"github.com/jinzhu/copier"
"github.com/rs/zerolog/log"
"github.com/httprunner/hrp/internal/boomer"
@@ -45,6 +46,11 @@ func (b *HRPBoomer) Run(testcases ...ITestCase) {
if err != nil {
panic(err)
}
cfg := testcase.Config.ToStruct()
err = initParameterIterator(cfg, "boomer")
if err != nil {
panic(err)
}
task := b.convertBoomerTask(testcase)
taskSlice = append(taskSlice, task)
}
@@ -63,9 +69,21 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase) *boomer.Task {
testcaseSuccess := true // flag whole testcase result
var transactionSuccess = true // flag current transaction result
cfg := testcase.Config.ToStruct()
caseConfig := &TConfig{}
// copy config to avoid data racing
if err := copier.Copy(caseConfig, cfg); err != nil {
log.Error().Err(err).Msg("copy config data failed")
}
// iterate through all parameter iterators and update case variables
for _, it := range caseConfig.ParametersSetting.Iterators {
if it.HasNext() {
caseConfig.Variables = mergeVariables(it.Next(), caseConfig.Variables)
}
}
startTime := time.Now()
for index, step := range testcase.TestSteps {
stepData, err := runner.runStep(index)
stepData, err := runner.runStep(index, caseConfig)
if err != nil {
// step failed
var elapsed int64

44
docs/BUILTIN.md Normal file
View File

@@ -0,0 +1,44 @@
# Builtin
## Assertion Methods
### Usage
In "teststeps" of each json/yaml testcase, the "validate" part contains four fields: "check", "assert", "expect" and
"msg", when using assertion methods, method name should be put in "assert" field. The assertion result of "check"
element will be checked out using the regulation you put in "assert" field and compared with the element in "expect"
field.
### Method List
- equals: assert the element to check equals the expected element.
- equal: alias for equals.
- greater_than: assert the element to check is greater than the expected element.
- less_than: assert the element to check is less than the expected element.
- greater_or_equals: assert the element to check is greater than or equal with the expected element.
- less_or_equals: assert the element to check is less than or equal with the expected element.
- not_equal: assert the element to check is not equal with the expected element.
- contained_by: assert the expected element contains the element to check.
- regex_match: assert the element to check matches the expected element using regex.
- type_match: assert the element to check matches the expected element in type.
- startswith: assert the element to check starts with the expected element.
- endswith: assert the element to check ends with the expected element.
- length_equals: assert the length of the element to check is equal with the expected element.
- length_equal: alias for length_equals.
- contains: assert the element to check contains the expected element.
- string_equals: assert the string is equal with the expected string.
## Common Functions
### Usage
The common functions are useful during the variables configuration, you can use "${FUNCTION_NAME}" to call the specific
function to define variables.
### Function List
- get_timestamp: get the thirteen-digit timestamp of current time. (call without argument)
- sleep: sleep n seconds to simulate the thinking time. (call with one argument n)
- gen_random_string: get the n-digit random string. (call with one argument n)
- max: get the maximum of two numbers m and n. (call with two argument m and n)
- md5: get the MD5 of the input string s. (call with one argument s)

View File

@@ -1,5 +1,10 @@
# Release History
## v0.4.0 (2022-01-05)
- feat: implement `parameterize` mechanism for data driven
- feat: add multiple builtin assertion methods
## v0.3.1 (2021-12-30)
- fix: set ulimit to 10240 before load testing

4
examples/account.csv Normal file
View File

@@ -0,0 +1,4 @@
username,password
test1,111111
test2,222222
test3,333333
1 username password
2 test1 111111
3 test2 222222
4 test3 333333

View File

@@ -19,14 +19,18 @@ func TestCaseCallFunction(t *testing.T) {
TestSteps: []hrp.IStep{
hrp.NewStep("get with params").
GET("/get").
WithParams(map[string]interface{}{"foo1": "${gen_random_string($n)}", "foo2": "${max($a, $b)}"}).
WithParams(map[string]interface{}{"foo1": "${gen_random_string($n)}", "foo2": "${max($a, $b)}", "foo3": "Foo3"}).
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}).
Extract().
WithJmesPath("body.args.foo1", "varFoo1").
Validate().
AssertEqual("status_code", 200, "check status code").
AssertLengthEqual("body.args.foo1", 5, "check args foo1").
AssertEqual("body.args.foo2", "12.3", "check args foo2"), // notice: request params value will be converted to string
AssertEqual("body.args.foo2", "12.3", "check args foo2").
AssertTypeMatch("body.args.foo3", "str", "check args foo3 is type string").
AssertStringEqual("body.args.foo3", "foo3", "check args foo3 case-insensitivity").
AssertContains("body.args.foo3", "Foo", "check contains ").
AssertContainedBy("body.args.foo3", "this is Foo3 test", "check contained by"), // notice: request params value will be converted to string
hrp.NewStep("post json data with functions").
POST("/post").
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}).

View File

@@ -0,0 +1,61 @@
{
"config": {
"name": "request methods testcase: validate with parameters",
"parameters": {
"user_agent": [
"iOS/10.1",
"iOS/10.2"
],
"username-password": "${parameterize(examples/account.csv)}"
},
"parameters_setting": {
"strategy": {
"user_agent": "sequential",
"username-password": "random"
},
"iteration": 6
},
"variables": {
"app_version": "v1",
"user_agent": "iOS/10.3"
},
"base_url": "https://postman-echo.com",
"verify": false
},
"teststeps": [
{
"name": "get with params",
"variables": {
"foo1": "$username",
"foo2": "$password",
"foo3": "$user_agent"
},
"request": {
"method": "GET",
"url": "/get",
"params": {
"foo1": "$foo1",
"foo2": "$foo2",
"foo3": "$foo3"
},
"headers": {
"User-Agent": "$user_agent,$app_version"
}
},
"validate": [
{
"check": "status_code",
"assert": "equals",
"expect": 200,
"msg": "check status code"
},
{
"check": "body.args.foo3",
"assert": "not_equal",
"expect": "iOS/10.3",
"msg": "check app version"
}
]
}
]
}

View File

@@ -0,0 +1,40 @@
config:
name: "request methods testcase: validate with parameters"
parameters:
user_agent: [ "iOS/10.1", "iOS/10.2" ]
username-password: ${parameterize(examples/account.csv)}
parameters_setting:
strategy:
user_agent: "sequential"
username-password: "random"
iteration: 6
variables:
app_version: v1
user_agent: iOS/10.3
base_url: "https://postman-echo.com"
verify: False
teststeps:
- name: get with params
variables:
foo1: $username
foo2: $password
foo3: $user_agent
request:
method: GET
url: /get
params:
foo1: $foo1
foo2: $foo2
foo3: $foo3
headers:
User-Agent: $user_agent,$app_version
validate:
- check: status_code
assert: equals
expect: 200
msg: check status code
- check: body.args.foo3
assert: not_equal
expect: iOS/10.3
msg: check app version

View File

@@ -2,6 +2,7 @@ package builtin
import (
"fmt"
"reflect"
"strings"
"github.com/stretchr/testify/assert"
@@ -15,13 +16,20 @@ var Assertions = map[string]func(t assert.TestingT, expected interface{}, actual
"greater_or_equals": assert.GreaterOrEqual,
"less_or_equals": assert.LessOrEqual,
"not_equal": assert.NotEqual,
"contains": assert.Contains,
"contained_by": assert.Contains,
"regex_match": assert.Regexp,
"type_match": assert.IsType,
// custom assertions
"startswith": StartsWith, // check if string starts with substring
"endswith": EndsWith, // check if string ends with substring
"length_equals": EqualLength,
"length_equal": EqualLength, // alias for length_equals
"startswith": StartsWith, // check if string starts with substring
"endswith": EndsWith, // check if string ends with substring
"length_equals": EqualLength,
"length_equal": EqualLength, // alias for length_equals
"length_less_than": LessThanLength,
"length_less_or_equals": LessOrEqualsLength,
"length_greater_than": GreaterThanLength,
"length_greater_or_equals": GreaterOrEqualsLength,
"contains": Contains,
"string_equals": EqualString,
}
func StartsWith(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
@@ -57,6 +65,66 @@ func EqualLength(t assert.TestingT, expected, actual interface{}, msgAndArgs ...
return assert.Len(t, actual, length, msgAndArgs...)
}
func GreaterThanLength(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
length, err := convertInt(expected)
if err != nil {
return assert.Fail(t, fmt.Sprintf("expected type is not int, got %#v", expected), msgAndArgs...)
}
ok, l := getLen(actual)
if !ok {
return assert.Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", actual), msgAndArgs...)
}
if l <= length {
return assert.Fail(t, fmt.Sprintf("\"%s\" should be more than %d item(s), but has %d", actual, length, l), msgAndArgs...)
}
return true
}
func GreaterOrEqualsLength(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
length, err := convertInt(expected)
if err != nil {
return assert.Fail(t, fmt.Sprintf("expected type is not int, got %#v", expected), msgAndArgs...)
}
ok, l := getLen(actual)
if !ok {
return assert.Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", actual), msgAndArgs...)
}
if l < length {
return assert.Fail(t, fmt.Sprintf("\"%s\" should be no less than %d item(s), but has %d", actual, length, l), msgAndArgs...)
}
return true
}
func LessThanLength(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
length, err := convertInt(expected)
if err != nil {
return assert.Fail(t, fmt.Sprintf("expected type is not int, got %#v", expected), msgAndArgs...)
}
ok, l := getLen(actual)
if !ok {
return assert.Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", actual), msgAndArgs...)
}
if l >= length {
return assert.Fail(t, fmt.Sprintf("\"%s\" should be less than %d item(s), but has %d", actual, length, l), msgAndArgs...)
}
return true
}
func LessOrEqualsLength(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
length, err := convertInt(expected)
if err != nil {
return assert.Fail(t, fmt.Sprintf("expected type is not int, got %#v", expected), msgAndArgs...)
}
ok, l := getLen(actual)
if !ok {
return assert.Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", actual), msgAndArgs...)
}
if l > length {
return assert.Fail(t, fmt.Sprintf("\"%s\" should be no more than %d item(s), but has %d", actual, length, l), msgAndArgs...)
}
return true
}
func convertInt(value interface{}) (int, error) {
switch v := value.(type) {
case int:
@@ -83,3 +151,32 @@ func convertInt(value interface{}) (int, error) {
return 0, fmt.Errorf("unsupported int convertion for %v(%T)", v, v)
}
}
// Contains assert whether actual element contains expected element
func Contains(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
return assert.Contains(t, actual, expected, msgAndArgs)
}
func EqualString(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
if !assert.IsType(t, "string", actual, msgAndArgs) {
return false
}
if !assert.IsType(t, "string", expected, msgAndArgs) {
return false
}
actualString := actual.(string)
expectedString := expected.(string)
return assert.True(t, strings.EqualFold(actualString, expectedString), msgAndArgs)
}
// getLen try to get length of object.
// return (false, 0) if impossible.
func getLen(x interface{}) (ok bool, length int) {
v := reflect.ValueOf(x)
defer func() {
if e := recover(); e != nil {
ok = false
}
}()
return true, v.Len()
}

View File

@@ -61,3 +61,80 @@ func TestEqualLength(t *testing.T) {
}
}
}
func TestLessThanLength(t *testing.T) {
testData := []struct {
raw interface{}
expected int
}{
{"", 1},
{[]string{}, 1},
{map[string]interface{}{}, 1},
{"a", 2},
{[]string{"a"}, 2},
{map[string]interface{}{"a": 123}, 2},
}
for _, data := range testData {
if !assert.True(t, LessThanLength(t, data.expected, data.raw)) {
t.Fail()
}
}
}
func TestLessOrEqualsLength(t *testing.T) {
testData := []struct {
raw interface{}
expected int
}{
{"", 1},
{[]string{}, 1},
{map[string]interface{}{"A": 111}, 1},
{"a", 1},
{[]string{"a"}, 2},
{map[string]interface{}{"a": 123}, 2},
}
for _, data := range testData {
if !assert.True(t, LessOrEqualsLength(t, data.expected, data.raw)) {
t.Fail()
}
}
}
func TestGreaterThanLength(t *testing.T) {
testData := []struct {
raw interface{}
expected int
}{
{"abcd", 3},
{[]string{"a", "b", "c"}, 2},
{map[string]interface{}{"a": 123, "b": 223, "c": 323}, 2},
}
for _, data := range testData {
if !assert.True(t, GreaterThanLength(t, data.expected, data.raw)) {
t.Fail()
}
}
}
func TestGreaterOrEqualsLength(t *testing.T) {
testData := []struct {
raw interface{}
expected int
}{
{"abcd", 3},
{[]string{"w"}, 1},
{map[string]interface{}{"A": 111}, 1},
{"a", 1},
{[]string{"a", "b", "c"}, 2},
{map[string]interface{}{"a": 123, "b": 223, "c": 323}, 2},
}
for _, data := range testData {
if !assert.True(t, GreaterOrEqualsLength(t, data.expected, data.raw)) {
t.Fail()
}
}
}

View File

@@ -2,10 +2,16 @@ package builtin
import (
"crypto/md5"
"encoding/csv"
"encoding/hex"
"io/ioutil"
"math"
"math/rand"
"path/filepath"
"strings"
"time"
"github.com/rs/zerolog/log"
)
var Functions = map[string]interface{}{
@@ -13,7 +19,9 @@ var Functions = map[string]interface{}{
"sleep": sleep, // call with one argument
"gen_random_string": genRandomString, // call with one argument
"max": math.Max, // call with two arguments
"md5": MD5,
"md5": MD5, // call with one argument
"parameterize": loadFromCSV,
"P": loadFromCSV,
}
func init() {
@@ -44,3 +52,33 @@ func MD5(str string) string {
hasher.Write([]byte(str))
return hex.EncodeToString(hasher.Sum(nil))
}
func loadFromCSV(path string) []map[string]interface{} {
path, err := filepath.Abs(path)
if err != nil {
log.Error().Str("path", path).Err(err).Msg("convert absolute path failed")
panic(err)
}
log.Info().Str("path", path).Msg("load csv file")
file, err := ioutil.ReadFile(path)
if err != nil {
log.Error().Err(err).Msg("load csv file failed")
panic(err)
}
r := csv.NewReader(strings.NewReader(string(file)))
content, err := r.ReadAll()
if err != nil {
log.Error().Err(err).Msg("parse csv file failed")
panic(err)
}
var result []map[string]interface{}
for i := 1; i < len(content); i++ {
row := make(map[string]interface{})
for j := 0; j < len(content[i]); j++ {
row[content[0][j]] = content[i][j]
}
result = append(result, row)
}
return result
}

View File

@@ -1,5 +1,11 @@
package hrp
import (
"math/rand"
"sync"
"time"
)
const (
httpGET string = "GET"
httpHEAD string = "HEAD"
@@ -13,13 +19,68 @@ const (
// TConfig represents config data structure for testcase.
// Each testcase should contain one config part.
type TConfig struct {
Name string `json:"name" yaml:"name"` // required
Verify bool `json:"verify,omitempty" yaml:"verify,omitempty"`
BaseURL string `json:"base_url,omitempty" yaml:"base_url,omitempty"`
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"`
Export []string `json:"export,omitempty" yaml:"export,omitempty"`
Weight int `json:"weight,omitempty" yaml:"weight,omitempty"`
Name string `json:"name" yaml:"name"` // required
Verify bool `json:"verify,omitempty" yaml:"verify,omitempty"`
BaseURL string `json:"base_url,omitempty" yaml:"base_url,omitempty"`
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"`
Export []string `json:"export,omitempty" yaml:"export,omitempty"`
Weight int `json:"weight,omitempty" yaml:"weight,omitempty"`
}
type TParamsConfig struct {
Strategy interface{} `json:"strategy,omitempty" yaml:"strategy,omitempty"`
Iteration int `json:"iteration,omitempty" yaml:"iteration,omitempty"`
Iterators []*Iterator `json:"parameterIterator,omitempty" yaml:"parameterIterator,omitempty"` //保存参数的迭代器
}
const (
strategyRandom string = "random"
strategySequential string = "Sequential"
)
type paramsType []map[string]interface{}
type Iterator struct {
sync.Mutex
data paramsType
strategy string // random, sequential
iteration int
index int
}
func (params paramsType) Iterator() *Iterator {
return &Iterator{
data: params,
iteration: len(params),
index: 0,
}
}
func (iter *Iterator) HasNext() bool {
if iter.iteration == -1 {
return true
}
return iter.index < iter.iteration
}
func (iter *Iterator) Next() (value map[string]interface{}) {
iter.Lock()
defer iter.Unlock()
if len(iter.data) == 0 {
iter.index++
return map[string]interface{}{}
}
if iter.strategy == strategyRandom {
randSource := rand.New(rand.NewSource(time.Now().Unix()))
randIndex := randSource.Intn(len(iter.data))
value = iter.data[randIndex]
} else {
value = iter.data[iter.index%len(iter.data)]
}
iter.index++
return value
}
// Request represents HTTP request data structure.

178
parser.go
View File

@@ -9,6 +9,7 @@ import (
"strings"
"github.com/maja42/goval"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/hrp/internal/builtin"
@@ -255,7 +256,6 @@ func callFunc(funcName string, arguments ...interface{}) (interface{}, error) {
// function not found
return nil, fmt.Errorf("function %s is not found", funcName)
}
funcValue := reflect.ValueOf(function)
if funcValue.Kind() != reflect.Func {
// function not valid
@@ -493,3 +493,179 @@ func findallVariables(raw string) variableSet {
return varSet
}
func genCartesianProduct(paramsMap map[string]paramsType) paramsType {
if len(paramsMap) == 0 {
return nil
}
var params []paramsType
for _, v := range paramsMap {
params = append(params, v)
}
var cartesianProduct paramsType
cartesianProduct = params[0]
for i := 0; i < len(params)-1; i++ {
var tempProduct paramsType
for _, param1 := range cartesianProduct {
for _, param2 := range params[i+1] {
tempProduct = append(tempProduct, mergeVariables(param1, param2))
}
}
cartesianProduct = tempProduct
}
return cartesianProduct
}
func parseParameters(parameters map[string]interface{}, variablesMapping map[string]interface{}) (map[string]paramsType, error) {
if len(parameters) == 0 {
return nil, nil
}
parsedParametersSlice := make(map[string]paramsType)
var err error
for k, v := range parameters {
var parameterSlice paramsType
rawValue := reflect.ValueOf(v)
switch rawValue.Kind() {
case reflect.String:
// e.g. username-password: ${parameterize(examples/account.csv)} -> [{"username": "test1", "password": "111111"}, {"username": "test2", "password": "222222"}]
var parsedParameterContent interface{}
parsedParameterContent, err = parseString(rawValue.String(), variablesMapping)
if err != nil {
log.Error().Interface("parameterContent", rawValue).Msg("[parseParameters] parse parameter content error")
return nil, err
}
parsedParameterRawValue := reflect.ValueOf(parsedParameterContent)
if parsedParameterRawValue.Kind() != reflect.Slice {
log.Error().Interface("parameterContent", parsedParameterRawValue).Msg("[parseParameters] parsed parameter content should be slice")
return nil, errors.New("parsed parameter content should be slice")
}
parameterSlice, err = parseSlice(k, parsedParameterRawValue.Interface())
case reflect.Slice:
// e.g. user_agent: ["iOS/10.1", "iOS/10.2"] -> [{"user_agent": "iOS/10.1"}, {"user_agent": "iOS/10.2"}]
parameterSlice, err = parseSlice(k, rawValue.Interface())
default:
log.Error().Interface("parameter", parameters).Msg("[parseParameters] parameter content should be slice or text(functions call)")
return nil, errors.New("parameter content should be slice or text(functions call)")
}
if err != nil {
return nil, err
}
parsedParametersSlice[k] = parameterSlice
}
return parsedParametersSlice, nil
}
func parseSlice(parameterName string, parameterContent interface{}) ([]map[string]interface{}, error) {
parameterNameSlice := strings.Split(parameterName, "-")
var parameterSlice []map[string]interface{}
parameterContentSlice := reflect.ValueOf(parameterContent)
if parameterContentSlice.Kind() != reflect.Slice {
return nil, errors.New("parameterContent should be slice")
}
for i := 0; i < parameterContentSlice.Len(); i++ {
parameterMap := make(map[string]interface{})
elem := reflect.ValueOf(parameterContentSlice.Index(i).Interface())
switch elem.Kind() {
case reflect.Map:
// e.g. "username-password": [{"username": "test1", "password": "passwd1", "other": "111"}, {"username": "test2", "password": "passwd2", "other": ""222}]
// -> [{"username": "test1", "password": "passwd1"}, {"username": "test2", "password": "passwd2"}]
for _, key := range parameterNameSlice {
if _, ok := elem.Interface().(map[string]interface{})[key]; ok {
parameterMap[key] = elem.MapIndex(reflect.ValueOf(key)).Interface()
} else {
log.Error().Interface("parameterNameSlice", parameterNameSlice).Msg("[parseParameters] parameter name not found")
return nil, errors.New("parameter name not found")
}
}
case reflect.Slice:
// e.g. "username-password": [["test1", "passwd1"], ["test2", "passwd2"]]
// -> [{"username": "test1", "password": "passwd1"}, {"username": "test2", "password": "passwd2"}]
if len(parameterNameSlice) != elem.Len() {
log.Error().Interface("parameterNameSlice", parameterNameSlice).Interface("parameterContent", elem.Interface()).Msg("[parseParameters] parameter name slice and parameter content slice should have the same length")
return nil, errors.New("parameter name slice and parameter content slice should have the same length")
} else {
for j := 0; j < elem.Len(); j++ {
parameterMap[parameterNameSlice[j]] = elem.Index(j).Interface()
}
}
default:
// e.g. "app_version": [3.1, 3.0]
// -> [{"app_version": 3.1}, {"app_version": 3.0}]
if len(parameterNameSlice) != 1 {
log.Error().Interface("parameterNameSlice", parameterNameSlice).Msg("[parseParameters] parameter name slice should have only one element when parameter content is string")
return nil, errors.New("parameter name slice should have only one element when parameter content is string")
}
parameterMap[parameterNameSlice[0]] = elem.Interface()
}
parameterSlice = append(parameterSlice, parameterMap)
}
return parameterSlice, nil
}
func initParameterIterator(cfg *TConfig, mode string) (err error) {
var parameters map[string]paramsType
parameters, err = parseParameters(cfg.Parameters, cfg.Variables)
if err != nil {
return err
}
// parse config parameters setting
if cfg.ParametersSetting == nil {
cfg.ParametersSetting = &TParamsConfig{Iterators: []*Iterator{}}
}
// boomer模式下不限制迭代次数
if mode == "boomer" {
cfg.ParametersSetting.Iteration = -1
}
rawValue := reflect.ValueOf(cfg.ParametersSetting.Strategy)
switch rawValue.Kind() {
case reflect.Map:
// strategy: {"user_agent": "sequential", "username-password": "random"}, 每个参数对应一个迭代器,每个迭代器随机、顺序选取元素互不影响
for k, v := range parameters {
if _, ok := rawValue.Interface().(map[string]interface{})[k]; ok {
// use strategy if configured
cfg.ParametersSetting.Iterators = append(
cfg.ParametersSetting.Iterators,
newIterator(v, rawValue.MapIndex(reflect.ValueOf(k)).Interface().(string), cfg.ParametersSetting.Iteration),
)
} else {
// use sequential strategy by default
cfg.ParametersSetting.Iterators = append(
cfg.ParametersSetting.Iterators,
newIterator(v, strategySequential, cfg.ParametersSetting.Iteration),
)
}
}
case reflect.String:
// strategy: random, 仅生成一个的迭代器该迭代器在参数笛卡尔积slice中随机选取元素
if len(rawValue.String()) == 0 {
cfg.ParametersSetting.Strategy = strategySequential
} else {
cfg.ParametersSetting.Strategy = strings.ToLower(rawValue.String())
}
cfg.ParametersSetting.Iterators = append(
cfg.ParametersSetting.Iterators,
newIterator(genCartesianProduct(parameters), cfg.ParametersSetting.Strategy.(string), cfg.ParametersSetting.Iteration),
)
default:
// default strategy: sequential, 仅生成一个的迭代器该迭代器在参数笛卡尔积slice中顺序选取元素
cfg.ParametersSetting.Strategy = strategySequential
cfg.ParametersSetting.Iterators = append(
cfg.ParametersSetting.Iterators,
newIterator(genCartesianProduct(parameters), cfg.ParametersSetting.Strategy.(string), cfg.ParametersSetting.Iteration),
)
}
return nil
}
func newIterator(parameters paramsType, strategy string, iteration int) *Iterator {
iter := parameters.Iterator()
iter.strategy = strategy
if iteration > 0 {
iter.iteration = iteration
} else if iteration < 0 {
iter.iteration = -1
} else if iter.iteration == 0 {
iter.iteration = 1
}
return iter
}

View File

@@ -618,3 +618,133 @@ func TestFindallVariables(t *testing.T) {
}
}
}
func TestParseParameters(t *testing.T) {
testData := []struct {
rawVars map[string]interface{}
expectLength int
}{
{
map[string]interface{}{
"username-password": "${parameterize(examples/account.csv)}",
"user_agent": []interface{}{"IOS/10.1", "IOS/10.2"}},
6,
},
{
map[string]interface{}{
"username-password": [][]interface{}{{"test1", "111111"}, {"test2", "222222"}, {"test3", "333333"}},
"user_agent": []interface{}{"IOS/10.1", "IOS/10.2"},
"app_version": []interface{}{0.3}},
6,
},
{
map[string]interface{}{
"username-password": [][]interface{}{{"test1", "111111"}, {"test2", "222222"}, {"test3", "333333"}},
"user_agent": []interface{}{"IOS/10.1", "IOS/10.2"},
"app_version": []interface{}{0.3, 0.4, 0.5}},
18,
},
{
map[string]interface{}{}, 0,
},
{
nil, 0,
},
}
for _, data := range testData {
params, _ := parseParameters(data.rawVars, map[string]interface{}{})
value := genCartesianProduct(params)
if !assert.Len(t, value, data.expectLength) {
t.Fail()
}
}
}
func TestParseParametersError(t *testing.T) {
testData := []struct {
rawVars map[string]interface{}
}{
{
map[string]interface{}{
"username_password": "${parameterize(examples/account.csv)}",
"user_agent": []interface{}{"IOS/10.1", "IOS/10.2"}},
},
{
map[string]interface{}{
"username-password": "${parameterize(examples/account.csv)}",
"user-agent": []interface{}{"IOS/10.1", "IOS/10.2"}},
},
{
map[string]interface{}{
"username-password": "${param(examples/account.csv)}",
"user_agent": []interface{}{"IOS/10.1", "IOS/10.2"}},
},
}
for _, data := range testData {
_, err := parseParameters(data.rawVars, map[string]interface{}{})
if !assert.Error(t, err) {
t.Fail()
}
}
}
func TestParseSlice(t *testing.T) {
testData := []struct {
rawVar1 string
rawVar2 interface{}
expect []map[string]interface{}
}{
{
"username-password",
[]map[string]interface{}{{"username": "test1", "password": 111111, "other": "111"}, {"username": "test2", "password": 222222, "other": "222"}},
[]map[string]interface{}{
{"username": "test1", "password": 111111},
{"username": "test2", "password": 222222},
},
},
{
"username-password",
[][]string{{"test1", "111111"}, {"test2", "222222"}},
[]map[string]interface{}{
{"username": "test1", "password": "111111"},
{"username": "test2", "password": "222222"},
},
},
{
"app_version",
[]float64{3.1, 3.0},
[]map[string]interface{}{
{"app_version": 3.1},
{"app_version": 3.0},
},
},
}
for _, data := range testData {
value, _ := parseSlice(data.rawVar1, data.rawVar2)
if !assert.Equal(t, data.expect, value) {
t.Fail()
}
}
}
func TestParseSliceError(t *testing.T) {
testData := []struct {
rawVar1 string
rawVar2 interface{}
}{
{
"app_version",
123,
},
{
"app_version",
"123",
},
}
for _, data := range testData {
_, err := parseSlice(data.rawVar1, data.rawVar2)
if !assert.Error(t, err) {
t.Fail()
}
}
}

View File

@@ -98,10 +98,26 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error {
log.Error().Err(err).Msg("[Run] convert ITestCase interface to TestCase struct failed")
return err
}
if err := r.newCaseRunner(testcase).run(); err != nil {
log.Error().Err(err).Msg("[Run] run testcase failed")
cfg := testcase.Config.ToStruct()
// parse config parameters
err = initParameterIterator(cfg, "runner")
if err != nil {
log.Error().Interface("parameters", cfg.Parameters).Err(err).Msg("parse config parameters failed")
return err
}
// 在runner模式下指定整体策略cfg.ParametersSetting.Iterators仅包含一个CartesianProduct的迭代器
for it := cfg.ParametersSetting.Iterators[0]; it.HasNext(); {
// iterate through all parameter iterators and update case variables
for _, it := range cfg.ParametersSetting.Iterators {
if it.HasNext() {
cfg.Variables = mergeVariables(it.Next(), cfg.Variables)
}
}
if err := r.newCaseRunner(testcase).run(); err != nil {
log.Error().Err(err).Msg("[Run] run testcase failed")
return err
}
}
}
return nil
}
@@ -141,16 +157,16 @@ func (r *caseRunner) run() error {
if err := r.parseConfig(config); err != nil {
return err
}
cfg := config.ToStruct()
log.Info().Str("testcase", config.Name()).Msg("run testcase start")
r.startTime = time.Now()
for index := range r.TestCase.TestSteps {
_, err := r.runStep(index)
_, err := r.runStep(index, cfg)
if err != nil {
if r.hrpRunner.failfast {
return errors.Wrap(err, "abort running due to failfast setting")
}
log.Warn().Err(err).Msg("run step failed, continue next step")
}
}
@@ -158,8 +174,7 @@ func (r *caseRunner) run() error {
return nil
}
func (r *caseRunner) runStep(index int) (stepResult *stepData, err error) {
config := r.TestCase.Config
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
@@ -179,23 +194,18 @@ func (r *caseRunner) runStep(index int) (stepResult *stepData, err error) {
log.Error().Err(err).Msg("copy step data failed")
return nil, err
}
copiedConfig := &TConfig{}
if err = copier.Copy(copiedConfig, config.ToStruct()); err != nil {
log.Error().Err(err).Msg("copy config data failed")
return nil, err
}
stepVariables := copiedStep.Variables
// override variables
// step variables > session variables (extracted variables from previous steps)
stepVariables = mergeVariables(stepVariables, r.sessionVariables)
// step variables > testcase config variables
stepVariables = mergeVariables(stepVariables, copiedConfig.Variables)
stepVariables = mergeVariables(stepVariables, caseConfig.Variables)
// parse step variables
parsedVariables, err := parseVariables(stepVariables)
if err != nil {
log.Error().Interface("variables", copiedConfig.Variables).Err(err).Msg("parse step variables failed")
log.Error().Interface("variables", caseConfig.Variables).Err(err).Msg("parse step variables failed")
return nil, err
}
copiedStep.Variables = parsedVariables // avoid data racing
@@ -212,7 +222,7 @@ func (r *caseRunner) runStep(index int) (stepResult *stepData, err error) {
}
} else {
// run request
copiedStep.Request.URL = buildURL(copiedConfig.BaseURL, copiedStep.Request.URL) // avoid data racing
copiedStep.Request.URL = buildURL(caseConfig.BaseURL, copiedStep.Request.URL) // avoid data racing
stepResult, err = r.runStepRequest(copiedStep)
if err != nil {
log.Error().Err(err).Msg("run request step failed")
@@ -484,7 +494,6 @@ func (r *caseRunner) parseConfig(config IConfig) error {
return err
}
cfg.Variables = parsedVariables
// parse config name
parsedName, err := parseString(cfg.Name, cfg.Variables)
if err != nil {

View File

@@ -79,10 +79,10 @@ func TestRunRequestRun(t *testing.T) {
TestSteps: []IStep{stepGET, stepPOSTData},
}
runner := NewRunner(t).SetDebug(true).newCaseRunner(testcase)
if _, err := runner.runStep(0); err != nil {
if _, err := runner.runStep(0, testcase.Config.ToStruct()); err != nil {
t.Fatalf("tStep.Run() error: %s", err)
}
if _, err := runner.runStep(1); err != nil {
if _, err := runner.runStep(1, testcase.Config.ToStruct()); err != nil {
t.Fatalf("tStepPOSTData.Run() error: %s", err)
}
}

View File

@@ -35,6 +35,94 @@ func (s *StepRequestValidation) AssertEqual(jmesPath string, expected interface{
return s
}
func (s *StepRequestValidation) AssertGreater(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "greater_than",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertLess(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "less_than",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertGreaterOrEqual(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "greater_or_equals",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertLessOrEqual(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "less_or_equals",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertNotEqual(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "not_equal",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertContains(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "contains",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertTypeMatch(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "type_match",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertRegexp(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "regex_match",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertStartsWith(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
@@ -67,3 +155,70 @@ func (s *StepRequestValidation) AssertLengthEqual(jmesPath string, expected inte
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertContainedBy(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "contained_by",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertLengthLessThan(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "length_less_than",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertStringEqual(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "string_equals",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertLengthLessOrEquals(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "length_less_or_equals",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertLengthGreaterThan(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "length_greater_than",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertLengthGreaterOrEquals(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "length_greater_or_equals",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}