HttpRunner用例格式

本文围绕httprunner测试用例展开,介绍了用例组成结构,包括测试步骤和配置,测试步骤可引入API步骤类型等完善。还说明了用例的编写格式,涉及测试步骤、配置和测试用例的编写格式及对应结构体。最后拓展讲解了用例加载方式,通过LoadTestCases方法将用例文件转为TestCase。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 用例组成结构

httprunner 最小的执行单元是 测试步骤(teststep)一个或多个测试步骤组成一个测试用例完整的测试用例,每个测试用例应该确保是可单独运行的。

在一个测试用例中除了包含 测试步骤(teststep)还有一个 配置(config)重点是存放:环境变量,测试的数据,设置base_url,请求中公共的配置...

在测试步骤中还可以通过引入 API 步骤类型和其他 TestCase 中定义的测试步骤进行完善补充用例的步骤

用例的编写格式

测试步骤(teststep)

编写格式

name: 步骤名称		
request:									    # 请求配置
  	method: POST 								# 请求方法
  	url: /service/mock					        # 请求url
  	http2: false 								# http2协议
  	params:  									# 请求参数(拼接到url后)
    		user_name: $user_name		        # 通过$引用变量
    		password: $password
    headers:
      	Content-Type: application/json	        # 请求头
    cookies:
      	Token: xxx								# 请求cookie
    body: {"":""}								# 请求体(最高优先级)
    json: {"":""}								#(第二优先级)使用时请求头自动设置Content-Type
    data: {"":""}								# 没有配置body和json时(第三优先级)
    timeout: 60									# 超时时间(单位:秒)
    allow_redirects: true				        # 允许重定向
    verify: false								# 客户端是否进行SSL校验(todo)
    upload: 									# 上传文件
      	fileName: x/xx/xx	
api: xx/xxx/xxx.yaml						    # 引入api类型步骤
testcase: xx/xxx/xxx.yaml				        # 引入其他testcase步骤
variables:										# 局部变量设置
  	user_name: zhangsan
  	password: xxxxxxxx
setup_hooks:									# 前置执行函数
  	-
    	"${SetupHook()}"					    # 通过${}引用函数
teardown_hooks:									# 后置执行函数
  	-
    	"${TearDownHook()}"
extract:										# 参数提取
  	task_id: body.task_id					
validate:									    # 响应断言
		- eq: ["status_code":200]		        # 响应码断言
export: 										# 变量导出(变量做为全局变量使用)	
    - task_id						
	

 对应结构体

type TStep struct {
    Name          string                 `json:"name" yaml:"name"` // required
    Request       *Request               `json:"request,omitempty" yaml:"request,omitempty"`
    API           interface{}            `json:"api,omitempty" yaml:"api,omitempty"`           // *APIPath or *API
    TestCase      interface{}            `json:"testcase,omitempty" yaml:"testcase,omitempty"` // *TestCasePath or *TestCase
    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"`
    WebSocket     *WebSocketAction       `json:"websocket,omitempty" yaml:"websocket,omitempty"`
    Android       *MobileStep            `json:"android,omitempty" yaml:"android,omitempty"`
    IOS           *MobileStep            `json:"ios,omitempty" yaml:"ios,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"`
}
type Request struct {
	Method         HTTPMethod             `json:"method" yaml:"method"` // required
	URL            string                 `json:"url" yaml:"url"`       // required
	HTTP2          bool                   `json:"http2,omitempty" yaml:"http2,omitempty"`
	Params         map[string]interface{} `json:"params,omitempty" yaml:"params,omitempty"`
	Headers        map[string]string      `json:"headers,omitempty" yaml:"headers,omitempty"`
	Cookies        map[string]string      `json:"cookies,omitempty" yaml:"cookies,omitempty"`
	Body           interface{}            `json:"body,omitempty" yaml:"body,omitempty"`
	Json           interface{}            `json:"json,omitempty" yaml:"json,omitempty"`
	Data           interface{}            `json:"data,omitempty" yaml:"data,omitempty"`
	Timeout        float64                `json:"timeout,omitempty" yaml:"timeout,omitempty"` // timeout in seconds
	AllowRedirects bool                   `json:"allow_redirects,omitempty" yaml:"allow_redirects,omitempty"`
	Verify         bool                   `json:"verify,omitempty" yaml:"verify,omitempty"`
	Upload         map[string]interface{} `json:"upload,omitempty" yaml:"upload,omitempty"`
}

请求参数: body data json

func convertCompatRequestBody(request *Request) {
    // 未设置body字段时,进入下面逻辑处理
	if request != nil && request.Body == nil {
        // 首先判断是否设置json字段,如果json字段有值则自动填充Content-Type
        // 同时将json字段赋给body,json 字段置空
		if request.Json != nil {
			if request.Headers == nil {
				request.Headers = make(map[string]string)
			}
			request.Headers["Content-Type"] = "application/json; charset=utf-8"
			request.Body = request.Json
			request.Json = nil
        // json 字段也没有时,校验data字段,data字段有值则赋给json,然后置空
		} else if request.Data != nil {
			request.Body = request.Data
			request.Data = nil
		}
	}
}

配置(config)

编写格式

config:
  	name: 配置名称
  	verify: false 											# 客户端是否进行SSL校验
  	base_url: http://www.baidu.com			                # 当前请求的基础 URL
  	headers:
      	Content-Type: application/json	                    # 请求头
  	environs:												# 环境变量(使用优先级:env文件 > 此处配置)
  	variables:												# 变量设置
      	user_name: zhangsan
  			password: xxxxxxxx
  	parameters:												# 参数化
    		user_agent: [ "iOS/10.1", "iOS/10.2" ]
    		user_name-password: 
          	- {"user_name": "test1", "password": "111111"}
          	- {"user_name": "test2", "password": "222222"}
  	parameters_setting:									    # 参数化策略
    		strategies:
      			user_agent:
        		name: "user-identity"
        		pick_order: "sequential"
      	username-password:
        		name: "user-info"
        		pick_order: "random"
    		limit: 6
  	timeout: 60												# 超时时间(秒)
  	export:													# 变量导出(变量做为全局变量使用)
  			- task_id			
 

对应结构体

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"`   // deprecated in v4.1, moved to env
	Headers           map[string]string      `json:"headers,omitempty" yaml:"headers,omitempty"`     // public request headers
	Environs          map[string]string      `json:"environs,omitempty" yaml:"environs,omitempty"`   // environment variables
	Variables         map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"` // global variables
	Parameters        map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"`
	ParametersSetting *TParamsConfig         `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"`
	ThinkTimeSetting  *ThinkTimeConfig       `json:"think_time,omitempty" yaml:"think_time,omitempty"`
	WebSocketSetting  *WebSocketConfig       `json:"websocket,omitempty" yaml:"websocket,omitempty"`
	IOS               []*uixt.IOSDevice      `json:"ios,omitempty" yaml:"ios,omitempty"`
	Android           []*uixt.AndroidDevice  `json:"android,omitempty" yaml:"android,omitempty"`
	Timeout           float64                `json:"timeout,omitempty" yaml:"timeout,omitempty"` // global timeout in seconds
	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
	PluginSetting     *PluginConfig          `json:"plugin,omitempty" yaml:"plugin,omitempty"` // plugin config
}

测试用例

编写格式

config:						# 编写配置
	name: 配置名称
	···
	
teststeps:				# 编写测试步骤
	-
  	name: 测试步骤1
  	···
  	
  -
    name: 测试步骤2
    ···

对应结构体

type TestCase struct {
	Config    *TConfig
	TestSteps []IStep
}

拓展:用例是如何加载的

实际在执行时是通过命令行执行要执行的用例文件路径执行,文件路径又是如何转换成测试用例的呢?
LoadTestCases 方法过滤接口类型,遍历目录树,获取所有待执行用例,在调用转换方法,转为TestCase

// 参数iTestCases是一个接口,其实现类只有两个:
// 一个是可执行的TestCase,另外一个就是表示文件路径的TestCasePath
func LoadTestCases(iTestCases ...ITestCase) ([]*TestCase, error) {
    testCases := make([]*TestCase, 0)

    for _, iTestCase := range iTestCases {

        // 如果iTestCase就是一个TestCase类型,则不需要特殊处理,转换后直接放入结果数组
        if _, ok := iTestCase.(*TestCase); ok {
            testcase, err := iTestCase.ToTestCase()
            if err != nil {
                log.Error().Err(err).Msg("failed to convert ITestCase interface to TestCase struct")
                return nil, err
            }
            testCases = append(testCases, testcase)
            continue
        }

        // iTestCase should be a TestCasePath, file path or folder path
        // 如果不是TestCase则iTestCase应该是 一个文件或文件夹路径,否则抛出异常
        tcPath, ok := iTestCase.(*TestCasePath)
        if !ok {
            return nil, errors.New("invalid iTestCase type")
        }

        casePath := tcPath.GetPath()
        // 获取用例的文件目录树,进行:遍历、过滤、转换 得到用例
        err := fs.WalkDir(os.DirFS(casePath), ".", func(path string, dir fs.DirEntry, e error) error {
            if dir == nil {
                // casePath is a file other than a dir
                path = casePath
            } else if dir.IsDir() && path != "." && strings.HasPrefix(path, ".") {
                // skip hidden folders
                return fs.SkipDir
            } else {
                // casePath is a dir
                path = filepath.Join(casePath, path)
            }

            // 判断用例文件后缀,目前只支持:yml yaml json
            ext := filepath.Ext(path)
            if ext != ".yml" && ext != ".yaml" && ext != ".json" {
                return nil
            }

            // filtered testcases
            testCasePath := TestCasePath(path)

            // 【重点】将文件路径转换为TestCase
            tc, err := testCasePath.ToTestCase()
            if err != nil {
                return nil
            }
            testCases = append(testCases, tc)
            return nil
        })
        if err != nil {
            return nil, errors.Wrap(err, "read dir failed")
        }
    }

    log.Info().Int("count", len(testCases)).Msg("load testcases successfully")
    return testCases, nil
}

实际将用例文件转为TestCase

调用栈

testCasePath.ToTestCase 读取文件内容映射hrp.TCase{}

TCase.ToTestCase 校验config和teststeps 不为空

TCase.toTestCase 实际处理方法最后返回:TestCase

func (tc *TCase) toTestCase() (*TestCase, error) {
    
	// ··· 省略代码

    // 遍历,处理没一个测试步骤
	for _, step := range tc.TestSteps {

        // 步骤里包含API类型步骤
		if step.API != nil {
			apiPath, ok := step.API.(string)
            // api 字段指定的是文件路径
			if ok {
				path := filepath.Join(projectRootDir, apiPath)
				if !builtin.IsFilePathExists(path) {
					return nil, errors.Wrap(code.ReferencedFileNotFound,
						fmt.Sprintf("referenced api file not found: %s", path))
				}

                // 将引入的api类型文件路径转换为API类型
				refAPI := APIPath(path)
				apiContent, err := refAPI.ToAPI()
				if err != nil {
					return nil, err
				}
				step.API = apiContent
			} else {
                // api 字段是一个完整的配置(map)
				apiMap, ok := step.API.(map[string]interface{})
				if !ok {
					return nil, errors.Wrap(code.InvalidCaseFormat,
						fmt.Sprintf("referenced api should be map or path(string), got %v", step.API))
				}
				api := &API{}
                // 将map转换为api
				err = mapstructure.Decode(apiMap, api)
				if err != nil {
					return nil, err
				}
				step.API = api
			}
            // 校验api字段是否为API类型,不是则抛出异常
			_, ok = step.API.(*API)
			if !ok {
				return nil, errors.Wrap(code.InvalidCaseFormat,
					fmt.Sprintf("failed to handle referenced API, got %v", step.TestCase))
			}
            // 将处理后的测试步骤加入测试用例中
			testCase.TestSteps = append(testCase.TestSteps, &StepAPIWithOptionalArgs{
				step: step,
			})

        // 步骤里包含了引用其他用例的步骤
		} else if step.TestCase != nil {
			casePath, ok := step.TestCase.(string)
            // 通过文件路径引入其他用例的步骤
			if ok {
				path := filepath.Join(projectRootDir, casePath)
				if !builtin.IsFilePathExists(path) {
					return nil, errors.Wrap(code.ReferencedFileNotFound,
						fmt.Sprintf("referenced testcase file not found: %s", path))
				}

                // 继续调用ToTestCase方法转TestCase
				refTestCase := TestCasePath(path)
				tc, err := refTestCase.ToTestCase()
				if err != nil {
					return nil, err
				}
				step.TestCase = tc
			} else {
                // testcase字段不是引入其他文件,而是一个步骤配置(map)
				testCaseMap, ok := step.TestCase.(map[string]interface{})
				if !ok {
					return nil, errors.Wrap(code.InvalidCaseFormat,
						fmt.Sprintf("referenced testcase should be map or path(string), got %v", step.TestCase))
				}
				tCase := &TCase{}
                // 将map转为tCase类型
				err = mapstructure.Decode(testCaseMap, tCase)
				if err != nil {
					return nil, err
				}
                // 递归调用转为TestCase
				tc, err := tCase.toTestCase()
				if err != nil {
					return nil, err
				}
				step.TestCase = tc
			}

            // 校验testcase字段是否为TestCase类型,不是则抛出异常
			_, ok = step.TestCase.(*TestCase)
			if !ok {
				return nil, errors.Wrap(code.InvalidCaseFormat,
					fmt.Sprintf("failed to handle referenced testcase, got %v", step.TestCase))
			}
            // 将测试步骤加入到测试用例
			testCase.TestSteps = append(testCase.TestSteps, &StepTestCaseWithOptionalArgs{
				step: step,
			})
		} 
        // ··· 省略代码
        
        else {
			log.Warn().Interface("step", step).Msg("[convertTestCase] unexpected step")
		}
	}
	return testCase, nil
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值