Merge pull request #786 from httprunner/leo_dev

2.4.1

- feat: add `upload` keyword for upload test, see [doc](https://docs.httprunner.org/prepare/upload-case/)
- test: pip install package 
- test: hrun command

**Fixed**

- fix: typo testfile_paths
- fix: check if locustio installed
- fix: dump json file name is empty when running relative testfile
This commit is contained in:
debugtalk
2019-12-12 22:37:25 +08:00
committed by GitHub
19 changed files with 313 additions and 106 deletions

View File

@@ -13,7 +13,11 @@ matrix:
install:
- pip install poetry
- poetry install -vvv
- poetry build
- ls dist/*.whl | xargs pip install # test installation
script:
- hrun -V
- cd tests/httpbin && hrun basic.yml --log-level debug --failfast && cd -
- python -m httprunner.cli hrun -V
- python -m httprunner.cli hrun -h
- poetry build

View File

@@ -1,5 +1,19 @@
# Release History
## 2.4.1 (2019-12-12)
**Added**
- feat: add `upload` keyword for upload test, see [doc](https://docs.httprunner.org/prepare/upload-case/)
- test: pip install package
- test: hrun command
**Fixed**
- fix: typo testfile_paths
- fix: check if locustio installed
- fix: dump json file name is empty when running relative testfile
## 2.4.0 (2019-12-11)
**Added**

View File

@@ -0,0 +1,51 @@
对于上传文件类型的测试场景HttpRunner 集成 [requests_toolbelt][1] 实现了上传功能。
在使用之前,确保已安装如下依赖库:
- [requests_toolbelt](https://github.com/requests/toolbelt)
- [filetype](https://github.com/h2non/filetype.py)
使用内置 `upload` 关键字可轻松实现上传功能适用版本2.4.1+)。
```yaml
- test:
name: upload file
request:
url: http://httpbin.org/upload
method: POST
headers:
Cookie: session=AAA-BBB-CCC
upload:
file: "data/file_to_upload"
field1: "value1"
field2: "value2"
validate:
- eq: ["status_code", 200]
```
同时你也可以继续使用之前描述形式适用版本2.0+)。
```yaml
- test:
name: upload file
variables:
file: "data/file_to_upload"
field1: "value1"
field2: "value2"
m_encoder: ${multipart_encoder(file=$file, field1=$field1, field2=$field2)}
request:
url: http://httpbin.org/upload
method: POST
headers:
Content-Type: ${multipart_content_type($m_encoder)}
Cookie: session=AAA-BBB-CCC
data: $m_encoder
validate:
- eq: ["status_code", 200]
```
参考案例:[httprunner/tests/httpbin/upload.v2.yml][2]
[1]: https://toolbelt.readthedocs.io/en/latest/uploading-data.html
[2]: https://github.com/httprunner/httprunner/blob/master/tests/httpbin/upload.v2.yml

View File

@@ -1,4 +1,4 @@
__version__ = "2.4.0"
__version__ = "2.4.1"
__description__ = "One-stop solution for HTTP(S) testing."
__all__ = ["__version__", "__description__"]

View File

@@ -3,19 +3,13 @@ Built-in functions used in YAML/JSON testcases.
"""
import datetime
import os
import random
import string
import time
import filetype
from requests_toolbelt import MultipartEncoder
from httprunner.compat import builtin_str, integer_types
from httprunner.exceptions import ParamsError
PWD = os.getcwd()
def gen_random_string(str_len):
""" generate random string with specified length
@@ -44,62 +38,3 @@ def sleep(n_secs):
"""
time.sleep(n_secs)
"""
upload files with requests-toolbelt
e.g.
- test:
name: upload file
variables:
file_path: "data/test.env"
multipart_encoder: ${multipart_encoder(file=$file_path)}
request:
url: /post
method: POST
headers:
Content-Type: ${multipart_content_type($multipart_encoder)}
data: $multipart_encoder
validate:
- eq: ["status_code", 200]
- startswith: ["content.files.file", "UserName=test"]
"""
def multipart_encoder(**kwargs):
""" initialize MultipartEncoder with uploading fields.
"""
def get_filetype(file_path):
file_type = filetype.guess(file_path)
if file_type:
return file_type.mime
else:
return "text/html"
fields_dict = {}
for key, value in kwargs.items():
if os.path.isabs(value):
_file_path = value
is_file = True
else:
global PWD
_file_path = os.path.join(PWD, value)
is_file = os.path.isfile(_file_path)
if is_file:
filename = os.path.basename(_file_path)
with open(_file_path, 'rb') as f:
mime_type = get_filetype(_file_path)
fields_dict[key] = (filename, f.read(), mime_type)
else:
fields_dict[key] = value
return MultipartEncoder(fields=fields_dict)
def multipart_content_type(multipart_encoder):
""" prepare Content-Type for request headers
"""
return multipart_encoder.content_type

View File

@@ -91,7 +91,7 @@ def main():
err_code = 0
try:
for path in args.testcase_paths:
for path in args.testfile_paths:
summary = runner.run(path, dot_env_path=args.dot_env_path)
report_dir = args.report_dir or os.path.join(runner.project_working_directory, "reports")
gen_html_report(

View File

@@ -9,6 +9,7 @@ HttpRunner loader
"""
from httprunner.loader.check import is_testcase_path, is_testcases, validate_json_file
from httprunner.loader.locate import get_project_working_directory as get_pwd
from httprunner.loader.load import load_csv_file, load_builtin_functions
from httprunner.loader.buildup import load_cases, load_project_data
@@ -16,6 +17,7 @@ __all__ = [
"is_testcase_path",
"is_testcases",
"validate_json_file",
"get_pwd",
"load_csv_file",
"load_builtin_functions",
"load_project_data",

View File

@@ -2,7 +2,6 @@ import importlib
import os
from httprunner import exceptions, logger, utils
from httprunner.builtin import functions
from httprunner.loader.load import load_module_functions, load_folder_content, load_file, load_dot_env_file, \
load_folder_files
from httprunner.loader.locate import init_project_working_directory, get_project_working_directory
@@ -335,7 +334,6 @@ def load_test_file(path):
"""
raw_content = load_file(path)
loaded_content = None
if isinstance(raw_content, dict):
@@ -480,11 +478,9 @@ def load_project_data(test_path, dot_env_path=None):
debugtalk_functions = {}
# locate PWD and load debugtalk.py functions
project_mapping["PWD"] = project_working_directory
functions.PWD = project_working_directory # TODO: remove
project_mapping["functions"] = debugtalk_functions
project_mapping["test_path"] = test_path
project_mapping["test_path"] = os.path.abspath(test_path)
# load api
tests_def_mapping["api"] = load_api_folder(os.path.join(project_working_directory, "api"))

View File

@@ -428,6 +428,11 @@ def get_mapping_function(function_name, functions_mapping):
elif function_name in ["environ", "ENV"]:
return utils.get_os_environ
elif function_name in ["multipart_encoder", "multipart_content_type"]:
# plugin for upload test
from httprunner.plugins import uploader
return getattr(uploader, function_name)
try:
# check if HttpRunner builtin functions
built_in_functions = loader.load_builtin_functions()
@@ -439,8 +444,9 @@ def get_mapping_function(function_name, functions_mapping):
# check if Python builtin functions
return getattr(builtins, function_name)
except AttributeError:
# is not builtin function
raise exceptions.FunctionNotFound("{} is not found.".format(function_name))
pass
raise exceptions.FunctionNotFound("{} is not found.".format(function_name))
def parse_function_params(params):
@@ -1146,13 +1152,18 @@ def __prepare_testcase_tests(tests, config, project_mapping, session_variables_s
api_def_dict = test_dict.pop("api_def")
_extend_with_api(test_dict, api_def_dict)
# verify priority: testcase teststep > testcase config
if "request" in test_dict:
if "verify" not in test_dict["request"]:
test_dict["request"]["verify"] = config_verify
if "upload" in test_dict["request"]:
from httprunner.plugins.uploader import prepare_upload_test
prepare_upload_test(test_dict)
# current teststep variables
teststep_variables_set |= set(test_dict.get("variables", {}).keys())
# verify priority: testcase teststep > testcase config
if "request" in test_dict and "verify" not in test_dict["request"]:
test_dict["request"]["verify"] = config_verify
# move extracted variable to session variables
if "extract" in test_dict:
extract_mapping = utils.ensure_mapping_format(test_dict["extract"])

View File

@@ -2,6 +2,7 @@ try:
# monkey patch ssl at beginning to avoid RecursionError when running locust.
from gevent import monkey
monkey.patch_ssl()
from locust import main as locust_main
except ImportError:
msg = """
Locust is not installed, install first and try again.
@@ -61,8 +62,7 @@ def gen_locustfile(testcase_file_path):
def start_locust_main():
from locust.main import main
main()
locust_main.main()
def start_master(sys_argv):

View File

@@ -0,0 +1,143 @@
""" upload test plugin.
If you want to use this plugin, you should install the following dependencies first.
- requests_toolbelt
- filetype
Then you can write upload test script as below:
- test:
name: upload file
request:
url: http://httpbin.org/upload
method: POST
headers:
Cookie: session=AAA-BBB-CCC
upload:
file: "data/file_to_upload"
field1: "value1"
field2: "value2"
validate:
- eq: ["status_code", 200]
For compatibility, you can also write upload test script in old way:
- test:
name: upload file
variables:
file: "data/file_to_upload"
field1: "value1"
field2: "value2"
m_encoder: ${multipart_encoder(file=$file, field1=$field1, field2=$field2)}
request:
url: http://httpbin.org/upload
method: POST
headers:
Content-Type: ${multipart_content_type($m_encoder)}
Cookie: session=AAA-BBB-CCC
data: $m_encoder
validate:
- eq: ["status_code", 200]
"""
import os
import sys
try:
import filetype
from requests_toolbelt import MultipartEncoder
except ImportError:
msg = """
uploader plugin dependencies uninstalled, install first and try again.
install with pip:
$ pip install requests_toolbelt filetype
"""
print(msg)
sys.exit(0)
from httprunner.exceptions import ParamsError
def prepare_upload_test(test_dict):
""" preprocess for upload test
replace `upload` info with MultipartEncoder
Args:
test_dict (dict):
{
"variables": {},
"request": {
"url": "http://httpbin.org/upload",
"method": "POST",
"headers": {
"Cookie": "session=AAA-BBB-CCC"
},
"upload": {
"file": "data/file_to_upload"
"md5": "123"
}
}
}
"""
upload_json = test_dict["request"].pop("upload", {})
if not upload_json:
raise ParamsError("invalid upload info: {}".format(upload_json))
params_list = []
for key, value in upload_json.items():
test_dict["variables"][key] = value
params_list.append("{}=${}".format(key, key))
params_str = ", ".join(params_list)
test_dict["variables"]["m_encoder"] = "${multipart_encoder(" + params_str + ")}"
test_dict["request"].setdefault("headers", {})
test_dict["request"]["headers"]["Content-Type"] = "${multipart_content_type($m_encoder)}"
test_dict["request"]["data"] = "$m_encoder"
def multipart_encoder(**kwargs):
""" initialize MultipartEncoder with uploading fields.
"""
def get_filetype(file_path):
file_type = filetype.guess(file_path)
if file_type:
return file_type.mime
else:
return "text/html"
fields_dict = {}
for key, value in kwargs.items():
if os.path.isabs(value):
# value is absolute file path
_file_path = value
is_exists_file = os.path.isfile(value)
else:
# value is not absolute file path, check if it is relative file path
from httprunner.loader import get_pwd
_file_path = os.path.join(get_pwd(), value)
is_exists_file = os.path.isfile(_file_path)
if is_exists_file:
# value is file path to upload
filename = os.path.basename(_file_path)
with open(_file_path, 'rb') as f:
mime_type = get_filetype(_file_path)
fields_dict[key] = (filename, f.read(), mime_type)
else:
fields_dict[key] = value
return MultipartEncoder(fields=fields_dict)
def multipart_content_type(m_encoder):
""" prepare Content-Type for request headers
"""
return m_encoder.content_type

View File

@@ -579,6 +579,7 @@ def dump_json_file(json_data, json_file_abs_path):
json_data,
indent=4,
separators=(',', ':'),
encoding="utf8",
ensure_ascii=False,
cls=PythonObjectEncoder
))

View File

@@ -1,5 +1,9 @@
# require mkdocs-material 3.x
#
# pip install mkdocs
# pip install mkdocs-material
# Project information
site_name: HttpRunner V2.x 中文使用文档
site_description: HttpRunner V2.x User Documentation
@@ -64,6 +68,7 @@ nav:
- 参数化数据驱动: prepare/parameters.md
- Validate & Prettify: prepare/validate-pretty.md
- 信息安全: prepare/security.md
- 文件上传场景: prepare/upload-case.md
- 测试执行:
- 运行测试(CLI): run-tests/cli.md
- 测试报告: run-tests/report.md

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "httprunner"
version = "2.4.0"
version = "2.4.1"
description = "One-stop solution for HTTP(S) testing."
license = "Apache-2.0"
readme = "README.md"

View File

@@ -15,8 +15,8 @@ try:
except ImportError:
httpbin_app = None
HTTPBIN_HOST = "httpbin.org"
HTTPBIN_PORT = 443
HTTPBIN_SERVER = "https://{}:{}".format(HTTPBIN_HOST, HTTPBIN_PORT)
HTTPBIN_PORT = 80
HTTPBIN_SERVER = "http://{}:{}".format(HTTPBIN_HOST, HTTPBIN_PORT)
FLASK_APP_PORT = 5000
SECRET_KEY = "DebugTalk"

View File

@@ -2,14 +2,15 @@
name: basic test with httpbin
base_url: https://httpbin.org/
- test:
name: index
request:
url: /
method: GET
validate:
- eq: ["status_code", 200]
- contains: [content, "HTTP Request & Response Service"]
#- test:
# TODO: fix compatibility with Python 2.7, UnicodeDecodeError
# name: index
# request:
# url: /
# method: GET
# validate:
# - eq: ["status_code", 200]
# - contains: [content, "HTTP Request & Response Service"]
- test:
name: headers

View File

@@ -0,0 +1,30 @@
config:
name: test upload file with httpbin
base_url: ${get_httpbin_server()}
teststeps:
-
name: upload file
variables:
file_path: "data/test.env"
m_encoder: ${multipart_encoder(file=$file_path)}
request:
url: /post
method: POST
headers:
Content-Type: ${multipart_content_type($m_encoder)}
data: $m_encoder
validate:
- eq: ["status_code", 200]
- startswith: ["content.files.file", "UserName=test"]
-
name: upload file with keyword
request:
url: /post
method: POST
upload:
file: "data/test.env"
validate:
- eq: ["status_code", 200]
- startswith: ["content.files.file", "UserName=test"]

View File

@@ -6,14 +6,24 @@
name: upload file
variables:
file_path: "data/test.env"
multipart_encoder: ${multipart_encoder(file=$file_path)}
m_encoder: ${multipart_encoder(file=$file_path)}
request:
url: /post
method: POST
headers:
Content-Type: ${multipart_content_type($multipart_encoder)}
data: $multipart_encoder
Content-Type: ${multipart_content_type($m_encoder)}
data: $m_encoder
validate:
- eq: ["status_code", 200]
- startswith: ["content.files.file", "UserName=test"]
- test:
name: upload file with keyword
request:
url: /post
method: POST
upload:
file: "data/test.env"
validate:
- eq: ["status_code", 200]
- startswith: ["content.files.file", "UserName=test"]

View File

@@ -222,12 +222,17 @@ class TestHttpRunner(ApiServerUnittest):
self.assertIn("records", summary["details"][0])
def test_run_yaml_upload(self):
summary = self.runner.run("tests/httpbin/upload.yml")
self.assertTrue(summary["success"])
self.assertEqual(summary["stat"]["testcases"]["total"], 1)
self.assertEqual(summary["stat"]["teststeps"]["total"], 1)
self.assertIn("details", summary)
self.assertIn("records", summary["details"][0])
upload_cases_list = [
"tests/httpbin/upload.yml",
"tests/httpbin/upload.v2.yml"
]
for upload_case in upload_cases_list:
summary = self.runner.run(upload_case)
self.assertTrue(summary["success"])
self.assertEqual(summary["stat"]["testcases"]["total"], 1)
self.assertEqual(summary["stat"]["teststeps"]["total"], 2)
self.assertIn("details", summary)
self.assertIn("records", summary["details"][0])
def test_run_post_data(self):
testcases = [
@@ -550,12 +555,11 @@ class TestHttpRunner(ApiServerUnittest):
}
)
# def test_validate_response_content(self):
# # TODO: fix compatibility with Python 2.7
# testcase_file_path = os.path.join(
# os.getcwd(), 'tests/httpbin/basic.yml')
# summary = self.runner.run(testcase_file_path)
# self.assertTrue(summary["success"])
def test_validate_response_content(self):
testcase_file_path = os.path.join(
os.getcwd(), 'tests/httpbin/basic.yml')
summary = self.runner.run(testcase_file_path)
self.assertTrue(summary["success"])
def test_html_report_xss(self):
testcases = [