介绍
HttpRunner 是一款面向 HTTP(S) 协议的通用测试框架,只需编写维护一份 YAML/JSON 脚本,即可实现自动化测试、性能测试、线上监控、持续集成等多种测试需求。
特点
- 继承了Requests的全部特性,可轻松实现 HTTP(S) 的各种测试需求
- 使用YAML或JSON格式定义测试用例,并使用Pytest以简洁优雅的方式运行用例
- 支持使用HAR实现接口录制并生成测试用例
- 支持variables/ extract/ validate/hooks机制,以应对非常复杂的测试场景
- 使用debugtalk.py插件自定义函数,可以在测试用例的任何部分调用
- 使用Jmespath,更加方便对返回的json进行校验
- 通过Pytest的强大插件生态补充了httprunner的功能
- 使用Allure,让测试报告更加美观,可读性更强
- 通过与locust的结合,可以很方便利用httprunner进行接口性能测试
- 支持CLI命令,更可与持续集成工具(CI/CD)完美结合,如Jenkins
安装
本文使用的环境为python3.7+httprunner3.1.4
pip install httprunner==3.1.4
查看版本
httprunner -V # hrun -V
3.1.4
可能出现的错误和解决办法
pip install markupsafe==2.0.1 pydantic==1.8.2 click==8.0.2
# 1.ImportError: cannot import name 'soft_unicode' from 'markupsafe'
pip install markupsafe==2.0.1
# 2. parameters的参数化数据 每次只运行第一组的参数化数据
pip install pydantic==1.8.2
# 3.ImportError: cannot import name ‘_unicodefun’ from ‘click’
pip install click==8.0.2
创建项目
httprunner startproject httprunner_demo
用pycharm打开项目,创建目录api,data,testsuites,创建文件main.py.
文件夹及作用
- api 存放接口定义yml文件
- data 存放测试数据
- har 存放har文件,使用抓包工具导出
- reports 存放测试报告
- testcases 存放测试用例yml文件
- testsuites 存放测试套件yml文件
- .env 存放环境变量,如base_url
- debugtalk.py 写python脚本,在yml文件中调用
api接口
本文使用flask定义测试接口,用flasgger生成swagger接口文档,创建api_server.py文件。
安装环境:
pip install flask Flask-JWT-Extended
pip install flasgger
# 5000端口被占用时
netstat -ano | find "5000"
taskkill /f /im port
运行api_server,访问 http://localhost:5000/apidocs/
import os
from flask import Flask, request, jsonify
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity
from flasgger import Swagger, swag_from
from werkzeug.utils import secure_filename
app = Flask(__name__)
swagger = Swagger(app)
app.config['JWT_SECRET_KEY'] = '123456' # 在实际应用中请更改为安全的密钥
jwt = JWTManager(app)
UPLOAD_FOLDER = 'uploads'
ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif'}
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
# 用户信息存储,模拟数据库
users = {
'test@qq.com': {'password': '123456', 'name': 'test'}
}
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/login', methods=['POST'])
def login():
"""
User Login
---
parameters:
- name: email
in: formData
type: string
required: true
description: The user's email address
- name: password
in: formData
type: string
required: true
description: The user's password
responses:
200:
description: Login successful
401:
description: Invalid credentials
"""
data = request.form
email = data.get('email')
password = data.get('password')
if email in users and users[email]['password'] == password:
# Create an access token
access_token = create_access_token(identity=email)
return jsonify(
{'code': 200, 'data': {'access_token': access_token, 'message': 'Login successful'}, 'message': 'success'})
else:
return jsonify({'code': 401, 'data': {}, 'message': 'Invalid credentials'})
@app.route('/change_name', methods=['POST'])
@jwt_required()
@swag_from({
'security': [{'JWT': []}],
'parameters': [
{
'name': 'Authorization',
'in': 'header',
'type': 'string',
'required': True,
'default': 'Bearer ',
'description': 'JWT Authorization header',
},
{
'name': 'new_name',
'in': 'formData',
'type': 'string',
'required': True,
'description': 'The new name for the user',
},
],
'responses': {
200: {'description': 'Name changed successfully'},
401: {'description': 'Unauthorized'},
404: {'description': 'User not found'},
}
})
def change_name():
current_user = get_jwt_identity()
data = request.form
new_name = data.get('new_name')
if current_user in users:
users[current_user]['name'] = new_name
return jsonify({'code': 200, 'data': {'message': 'Name changed successfully'}, 'message': 'success'})
else:
return jsonify({'code': 404, 'data': {}, 'message': 'User not found'})
@app.route('/upload_file', methods=['POST'])
@jwt_required()
def upload_file():
"""
Upload User File
---
security:
- JWT: []
parameters:
- name: Authorization
in: header
type: string
required: true
default: 'Bearer '
description: JWT Authorization header
- name: file
in: formData
type: file
required: true
description: The file to upload
responses:
200:
description: File uploaded successfully
400:
description: No file provided or invalid file format
401:
description: Unauthorized
404:
description: User not found
"""
current_user = get_jwt_identity()
if 'file' not in request.files:
return jsonify({'code': 400, 'data': {}, 'message': 'No file provided'})
file = request.files['file']
if file.filename == '':
return jsonify({'code': 400, 'data': {}, 'message': 'No file provided'})
if file and allowed_file(file.filename):
if current_user in users:
# Save the uploaded file
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
# You can perform additional processing here (e.g., save file path to user profile)
return jsonify({'code': 200, 'data': {'message': 'File uploaded successfully', 'filename': filename},
'message': 'success'})
else:
return jsonify({'code': 404, 'data': {}, 'message': 'User not found'})
else:
return jsonify({'code': 400, 'data': {}, 'message': 'No file provided or invalid file format'})
if __name__ == '__main__':
app.run(debug=True, port=5000, load_dotenv=False)
接口定义yml文件
- 使用网页或postman发送登录请求
- 使用抓包工具fiddler或charles导出har文件(v1.1)存放har目录
- 使用命令har2case -2y har/login.har 转化成yml文件,存放api目录
- 修改login.yml,将teststeps的所有的变量存放config
- 在.env文件中添加base_url=http://127.0.0.1:5000
config:
name: testcase description
variables:
email: test@qq.com
password: '123456'
verify: false
base_url: ${ENV(base_url)}
teststeps:
- name: /login
request:
data:
email: $email
password: $password
headers:
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
Content-Length: '35'
Content-Type: application/x-www-form-urlencoded
Host: 127.0.0.1:5000
Origin: http://127.0.0.1:5000
Referer: http://127.0.0.1:5000/apidocs/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36
accept: application/json
sec-ch-ua: '"Google Chrome";v="119", "Chromium";v="119", "Not?A_Brand";v="24"'
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: '"Windows"'
method: POST
url: /login
extract:
- access_token: body.data.access_token
validate:
- eq:
- status_code
- 200
- eq:
- headers.Content-Type
- application/json
- eq:
- body.code
- 200
- eq:
- body.message
- success
- 使用extract关键字提取access_token,用来接口关联
- 使用$变量名调用config下的全局变量,${ENV(变量名)}调用环境变量,${函数名()}调用debugtalk.py中定义的函数。
测试用例yml文件
login_case.yml,测试步骤里面调用api
config:
name: testcase description
teststeps:
- name: /login
api: api/login.yml
在main.py文件添加,运行测试用例,生成测试报告
import os
if __name__ == '__main__':
os.system('hrun testcases/login_case.yml --html=reports/report.html')
接口关联
通过extract关键字提取,export关键字导出
- 调用change_name接口,按照同样的方式生成change_name.yml
config:
name: testcase description
variables:
authorization: ${get_authorization($access_token)}
new_name: 'test123'
verify: false
base_url: ${ENV(base_url)}
teststeps:
- name: /change_name
request:
data:
new_name: $new_name
headers:
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Authorization: $authorization
Connection: keep-alive
Content-Length: '16'
Content-Type: application/x-www-form-urlencoded
Host: 127.0.0.1:5000
Origin: http://127.0.0.1:5000
Referer: http://127.0.0.1:5000/apidocs/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36
accept: application/json
sec-ch-ua: '"Google Chrome";v="119", "Chromium";v="119", "Not?A_Brand";v="24"'
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: '"Windows"'
method: POST
url: /change_name
validate:
- eq:
- status_code
- 200
- eq:
- headers.Content-Type
- application/json
- eq:
- body.code
- 200
- eq:
- body.message
- success
在debugtalk.py中添加get_authorization
def get_authorization(access_token):
return 'Bearer ' + access_token
测试用例chage_name_case.yml
config:
name: testcase description
teststeps:
- name: /login
api: api/login.yml
export:
- access_token
- name: /change_name
api: api/change_name.yml
提取参数需要使用export导出,下一个步骤才能使用
数据驱动
使用parameters关键字,写在测试用例的config下
方式一
直接写到yaml文件,参数名用"-"连接
config:
name: testcase description
parameters:
email-password-body_code:
- [ "test@qq.com","123456",200 ]
- [ "","123456",401 ]
- [ "test@qq.com","",401 ]
teststeps:
- name: /login
api: api/login.yml
使用数据驱动需要修改对应api的断言,常用断言:eq,str_eq,contains
validate:
- eq:
- status_code
- 200
- str_eq:
- body.code
- $body_code
方式二
使用csv文件
config:
name: testcase description
parameters:
email-password-body_code: ${P(data/login_data.csv)}
teststeps:
- name: /login
api: api/login.yml
login_data.csv
email,password,body_code
"test@qq.com","123456",200
"","123456",401
"test@qq.com","",401
方式三
使用debugtalk.py文件,需要返回列表字典格式数据:
config:
name: testcase description
parameters:
email-password-body_code: ${login_data()}
teststeps:
- name: /login
api: api/login.yml
login_data
def login_data():
data = [
{"email": "test@qq.com", "password": "123456", "body_code": 200},
{"email": "", "password": "123456", "body_code": 401},
{"email": "test@qq.com", "password": "", "body_code": 401}
]
return data
文件上传
文件上传需要安装插件
pip install requests_toolbelt filetype
pip install "httprunner[upload]"
api,使用upload关键字上传,key名称为file,变量名尽量不要和key名一样,没有特殊符号等
config:
name: testcase description
variables:
file_path: data/test.jpg
authorization: ${get_authorization($access_token)}
verify: false
teststeps:
- name: /upload_file
request:
upload:
file: $file_path
headers:
Authorization: $authorization
method: POST
url: http://127.0.0.1:5000/upload_file
validate:
- eq:
- status_code
- 200
case
config:
name: testcase description
teststeps:
- name: /login
api: api/login.yml
export:
- access_token
- name: /upload_file
api: api/upload_file.yml
测试套件
运行指定测试用例集合,注意testcases
test_suite.yml
config:
name: 测试套件
testcases:
- name: /login
testcase: testcases/login_case.yml
- name: /change_name
testcase: testcases/change_name_case.yml
- name: /upload_file
testcase: testcases/upload_file_case.yml
执行测试套件
os.system('hrun testsuites/test_suite.yml --html=reports/report.html')
Allure测试报告
-
下载allure压缩包,解压后将bin目录添加到系统环境变量path
官网下载:https://github.com/allure-framework/allure2/releases
验证 allure --version,装完需重启pycharm
-
pip install allure-pytest
-
执行测试,生成allue报告
os.system('hrun testsuites/test_suite.yml --alluredir=reports/temp --clean-alluredir') os.system('allure generate reports/temp -o reports/allure --clean')