diff --git a/examples/httpbin/basic.yml b/examples/httpbin/basic.yml new file mode 100644 index 00000000..d27d8604 --- /dev/null +++ b/examples/httpbin/basic.yml @@ -0,0 +1,89 @@ +config: + name: basic test with httpbin + base_url: https://httpbin.org/ + +teststeps: +- + name: headers + request: + url: /headers + method: GET + validate: + - eq: ["status_code", 200] + - eq: [body.headers.Host, "httpbin.org"] + +- + name: user-agent + request: + url: /user-agent + method: GET + validate: + - eq: ["status_code", 200] +# - startswith: [body.user-agent, "python-requests"] + +- + name: get without params + request: + url: /get + method: GET + validate: + - eq: ["status_code", 200] + - eq: [body.args, {}] + +- + name: get with params in url + request: + url: /get?a=1&b=2 + method: GET + validate: + - eq: ["status_code", 200] + - eq: [body.args, {'a': '1', 'b': '2'}] + +- + name: get with params in params field + request: + url: /get + params: + a: 1 + b: 2 + method: GET + validate: + - eq: ["status_code", 200] + - eq: [body.args, {'a': '1', 'b': '2'}] + +- + name: set cookie + request: + url: /cookies/set?name=value + method: GET + validate: + - eq: ["status_code", 200] + # - eq: [cookies.name, "value"] + +- + name: extract cookie + request: + url: /cookies + method: GET + validate: + - eq: ["status_code", 200] + # - eq: [cookies.name, "value"] + +- + name: post data + request: + url: /post + method: POST + headers: + Content-Type: application/json + data: abc + validate: + - eq: ["status_code", 200] + +- + name: validate body length + request: + url: /spec.json + method: GET + validate: + - len_eq: ["body", 9] diff --git a/examples/httpbin/basic_test.py b/examples/httpbin/basic_test.py new file mode 100644 index 00000000..26862a25 --- /dev/null +++ b/examples/httpbin/basic_test.py @@ -0,0 +1,96 @@ +# NOTICE: Generated By HttpRunner. DO'NOT EDIT! +from httprunner import HttpRunner, TConfig, TStep + + +class TestCaseBasic(HttpRunner): + config = TConfig( + **{ + "name": "basic test with httpbin", + "base_url": "https://httpbin.org/", + "path": "examples/httpbin/basic_test.py", + } + ) + + teststeps = [ + TStep( + **{ + "name": "headers", + "request": {"url": "/headers", "method": "GET"}, + "validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["body.headers.Host", "httpbin.org"]}, + ], + } + ), + TStep( + **{ + "name": "user-agent", + "request": {"url": "/user-agent", "method": "GET"}, + "validate": [{"eq": ["status_code", 200]}], + } + ), + TStep( + **{ + "name": "get without params", + "request": {"url": "/get", "method": "GET"}, + "validate": [{"eq": ["status_code", 200]}, {"eq": ["body.args", {}]}], + } + ), + TStep( + **{ + "name": "get with params in url", + "request": {"url": "/get?a=1&b=2", "method": "GET"}, + "validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["body.args", {"a": "1", "b": "2"}]}, + ], + } + ), + TStep( + **{ + "name": "get with params in params field", + "request": {"url": "/get", "params": {"a": 1, "b": 2}, "method": "GET"}, + "validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["body.args", {"a": "1", "b": "2"}]}, + ], + } + ), + TStep( + **{ + "name": "set cookie", + "request": {"url": "/cookies/set?name=value", "method": "GET"}, + "validate": [{"eq": ["status_code", 200]}], + } + ), + TStep( + **{ + "name": "extract cookie", + "request": {"url": "/cookies", "method": "GET"}, + "validate": [{"eq": ["status_code", 200]}], + } + ), + TStep( + **{ + "name": "post data", + "request": { + "url": "/post", + "method": "POST", + "headers": {"Content-Type": "application/json"}, + "data": "abc", + }, + "validate": [{"eq": ["status_code", 200]}], + } + ), + TStep( + **{ + "name": "validate body length", + "request": {"url": "/spec.json", "method": "GET"}, + "validate": [{"len_eq": ["body", 9]}], + } + ), + ] + + +if __name__ == "__main__": + TestCaseBasic().test_start() diff --git a/examples/httpbin/upload.yml b/examples/httpbin/upload.yml new file mode 100644 index 00000000..5eff4cbf --- /dev/null +++ b/examples/httpbin/upload.yml @@ -0,0 +1,30 @@ +config: + name: test upload file with httpbin + base_url: ${get_httpbin_server()} + +teststeps: +- + name: upload file + variables: + file_path: "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: ["body.files.file", "UserName=test"] + +- + name: upload file with keyword + request: + url: /post + method: POST + upload: + file: "test.env" + validate: + - eq: ["status_code", 200] + - startswith: ["body.files.file", "UserName=test"] diff --git a/examples/httpbin/upload_test.py b/examples/httpbin/upload_test.py new file mode 100644 index 00000000..ca418a4e --- /dev/null +++ b/examples/httpbin/upload_test.py @@ -0,0 +1,54 @@ +# NOTICE: Generated By HttpRunner. DO'NOT EDIT! +from httprunner import HttpRunner, TConfig, TStep + + +class TestCaseUpload(HttpRunner): + config = TConfig( + **{ + "name": "test upload file with httpbin", + "base_url": "${get_httpbin_server()}", + "path": "examples/httpbin/upload_test.py", + } + ) + + teststeps = [ + TStep( + **{ + "name": "upload file", + "variables": { + "file_path": "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": ["body.files.file", "UserName=test"]}, + ], + } + ), + TStep( + **{ + "name": "upload file with keyword", + "request": { + "url": "/post", + "method": "POST", + "upload": {"file": "test.env"}, + }, + "validate": [ + {"eq": ["status_code", 200]}, + {"startswith": ["body.files.file", "UserName=test"]}, + ], + } + ), + ] + + +if __name__ == "__main__": + TestCaseUpload().test_start() diff --git a/examples/httpbin/validate.yml b/examples/httpbin/validate.yml new file mode 100644 index 00000000..c45e2ffd --- /dev/null +++ b/examples/httpbin/validate.yml @@ -0,0 +1,35 @@ +config: + name: basic test with httpbin + base_url: http://httpbin.org/ + +teststeps: +- + name: validate response with json path + request: + url: /get + params: + a: 1 + b: 2 + method: GET + validate: + - eq: ["status_code", 200] + - eq: ["body.args.a", 1] + - eq: ["body.args.b", 2] + validate_script: + - "assert status_code == 200" + + +- + name: validate response with python script + request: + url: /get + params: + a: 1 + b: 2 + method: GET + validate: + - eq: ["status_code", 200] + validate_script: + - "assert status_code == 201" + - "a = response_json.get('args').get('a')" + - "assert a == '1'" diff --git a/examples/httpbin/validate_test.py b/examples/httpbin/validate_test.py new file mode 100644 index 00000000..e66796dd --- /dev/null +++ b/examples/httpbin/validate_test.py @@ -0,0 +1,43 @@ +# NOTICE: Generated By HttpRunner. DO'NOT EDIT! +from httprunner import HttpRunner, TConfig, TStep + + +class TestCaseValidate(HttpRunner): + config = TConfig( + **{ + "name": "basic test with httpbin", + "base_url": "http://httpbin.org/", + "path": "examples/httpbin/validate_test.py", + } + ) + + teststeps = [ + TStep( + **{ + "name": "validate response with json path", + "request": {"url": "/get", "params": {"a": 1, "b": 2}, "method": "GET"}, + "validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["body.args.a", 1]}, + {"eq": ["body.args.b", 2]}, + ], + "validate_script": ["assert status_code == 200"], + } + ), + TStep( + **{ + "name": "validate response with python script", + "request": {"url": "/get", "params": {"a": 1, "b": 2}, "method": "GET"}, + "validate": [{"eq": ["status_code", 200]}], + "validate_script": [ + "assert status_code == 201", + "a = response_json.get('args').get('a')", + "assert a == '1'", + ], + } + ), + ] + + +if __name__ == "__main__": + TestCaseValidate().test_start() diff --git a/httprunner/client.py b/httprunner/client.py index 0a99020c..b9606e3f 100644 --- a/httprunner/client.py +++ b/httprunner/client.py @@ -36,22 +36,22 @@ def get_req_resp_record(resp_obj: Response) -> ReqRespData: logger.debug(msg) # record actual request info + request_headers = dict(resp_obj.request.headers) + request_body = resp_obj.request.body + + if request_body: + request_content_type = lower_dict_keys(request_headers).get("content-type") + if request_content_type and "multipart/form-data" in request_content_type: + # upload file type + request_body = "upload file stream (OMITTED)" + request_data = RequestData( method=resp_obj.request.method, url=resp_obj.request.url, - headers=dict(resp_obj.request.headers), - body=resp_obj.request.body, + headers=request_headers, + body=request_body, ) - request_body = resp_obj.request.body - if request_body: - request_content_type = lower_dict_keys(request_data.headers).get("content-type") - if request_content_type and "multipart/form-data" in request_content_type: - # upload file type - request_data.body = "upload file stream (OMITTED)" - else: - request_data.body = request_body - # log request details in debug mode log_print(request_data, "request") diff --git a/httprunner/ext/uploader/__init__.py b/httprunner/ext/uploader/__init__.py index 7963ee15..97a36dc2 100644 --- a/httprunner/ext/uploader/__init__.py +++ b/httprunner/ext/uploader/__init__.py @@ -45,6 +45,9 @@ For compatibility, you can also write upload test script in old way: import os import sys +from httprunner.parser import parse_variables_mapping +from httprunner.schema import TStep, FunctionsMapping + try: import filetype from requests_toolbelt import MultipartEncoder @@ -57,16 +60,13 @@ $ pip install requests_toolbelt filetype print(msg) sys.exit(0) -from httprunner.exceptions import ParamsError - -def prepare_upload_test(test_dict): +def prepare_upload_step(step: TStep, functions: FunctionsMapping): """ preprocess for upload test replace `upload` info with MultipartEncoder Args: - test_dict (dict): - + step: teststep { "variables": {}, "request": { @@ -81,26 +81,26 @@ def prepare_upload_test(test_dict): } } } + functions: functions mapping """ - upload_json = test_dict["request"].pop("upload", {}) - if not upload_json: - raise ParamsError(f"invalid upload info: {upload_json}") + if not step.request.upload: + return params_list = [] - for key, value in upload_json.items(): - test_dict["variables"][key] = value + for key, value in step.request.upload.items(): + step.variables[key] = value params_list.append(f"{key}=${key}") params_str = ", ".join(params_list) - test_dict["variables"]["m_encoder"] = "${multipart_encoder(" + params_str + ")}" + step.variables["m_encoder"] = "${multipart_encoder(" + params_str + ")}" - test_dict["request"].setdefault("headers", {}) - test_dict["request"]["headers"][ - "Content-Type" - ] = "${multipart_content_type($m_encoder)}" + # parse variables + step.variables = parse_variables_mapping(step.variables, functions) - test_dict["request"]["data"] = "$m_encoder" + step.request.headers["Content-Type"] = "${multipart_content_type($m_encoder)}" + + step.request.data = "$m_encoder" def multipart_encoder(**kwargs): diff --git a/httprunner/loader_test.py b/httprunner/loader_test.py index 6cefbcb5..dac77804 100644 --- a/httprunner/loader_test.py +++ b/httprunner/loader_test.py @@ -125,4 +125,3 @@ class TestLoader(unittest.TestCase): loader.locate_file("examples/httpbin/", "debugtalk.py"), os.path.join(os.getcwd(), "examples/httpbin/debugtalk.py"), ) - diff --git a/httprunner/runner.py b/httprunner/runner.py index b61bfcf1..ab833ead 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -8,6 +8,7 @@ from loguru import logger from httprunner import utils, exceptions from httprunner.client import HttpSession from httprunner.exceptions import ValidationFailure, ParamsError +from httprunner.ext.uploader import prepare_upload_step from httprunner.loader import load_project_meta, load_testcase_file from httprunner.parser import build_url, parse_data, parse_variables_mapping from httprunner.response import ResponseObject @@ -53,7 +54,9 @@ class HttpRunner(object): step_data = StepData(name=step.name) # parse + prepare_upload_step(step, self.__project_meta.functions) request_dict = step.request.dict() + request_dict.pop("upload", None) parsed_request_dict = parse_data( request_dict, step.variables, self.__project_meta.functions ) diff --git a/httprunner/runner_test.py b/httprunner/runner_test.py index 9635ac4f..ca84584f 100644 --- a/httprunner/runner_test.py +++ b/httprunner/runner_test.py @@ -13,12 +13,8 @@ class TestHttpRunner(unittest.TestCase): ) result = self.runner.get_summary() self.assertTrue(result.success) - self.assertEqual( - result.name, "request methods testcase with variables" - ) - self.assertEqual( - result.step_datas[0].name, "get with params" - ) + self.assertEqual(result.name, "request methods testcase with variables") + self.assertEqual(result.step_datas[0].name, "get with params") self.assertEqual(len(result.step_datas), 3) def test_run_testcase_by_path_ref_testcase(self): @@ -27,10 +23,6 @@ class TestHttpRunner(unittest.TestCase): ) result = self.runner.get_summary() self.assertTrue(result.success) - self.assertEqual( - result.name, "request methods testcase: reference testcase" - ) - self.assertEqual( - result.step_datas[0].name, "request with variables" - ) + self.assertEqual(result.name, "request methods testcase: reference testcase") + self.assertEqual(result.step_datas[0].name, "request with variables") self.assertEqual(len(result.step_datas), 1) diff --git a/httprunner/schema.py b/httprunner/schema.py index 906e31a0..4ea1e797 100644 --- a/httprunner/schema.py +++ b/httprunner/schema.py @@ -56,6 +56,7 @@ class Request(BaseModel): timeout: int = 120 allow_redirects: bool = True verify: Verify = False + upload: Dict = {} # used for upload files class TStep(BaseModel): @@ -107,7 +108,7 @@ class RequestData(BaseModel): url: Url headers: Headers = {} # TODO: add cookies - body: Union[Text, Dict] = {} + body: Union[Text, bytes, Dict, None] = {} class ResponseData(BaseModel): @@ -137,6 +138,7 @@ class SessionData(BaseModel): class StepData(BaseModel): """teststep data, each step maybe corresponding to one request or one testcase""" + success: bool = False name: Text = "" # teststep name data: Union[SessionData, List[SessionData]] = None