├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── demo ├── __init__.py ├── api │ ├── __init__.py │ ├── cookies.py │ ├── html_form.py │ ├── http_methods.py │ ├── redirects.py │ └── response_formats.py ├── test_config.py ├── test_http_methods.py ├── test_parameters.py ├── test_redirects.py └── test_session.py ├── poetry.lock ├── pyproject.toml ├── pytest_requests ├── __init__.py ├── exceptions.py ├── http │ ├── __init__.py │ ├── request.py │ └── response.py ├── testcase.py └── utils.py └── tests ├── __init__.py └── test_utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - 3.5 5 | - 3.6 6 | matrix: 7 | include: 8 | - python: 3.7 9 | dist: xenial # Required for Python 3.7 10 | sudo: true # Required for Python 3.7 11 | install: 12 | - pip install poetry 13 | - poetry install -vvv 14 | script: 15 | - poetry run coverage run --source=pytest_requests -m pytest -v --showlocals 16 | - poetry run coverage report -m 17 | after_success: 18 | - poetry run coveralls -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release History 2 | 3 | ## 0.1.0 (2019-08-03) 4 | 5 | **Added** 6 | 7 | - init project with poetry, travis, pytest, coverage and coveralls 8 | - feat: define each api in subclass of `HttpRequest` 9 | - feat: define each testcase in function startswith `test_` 10 | - feat: prepare request, include params, headers, cookies, data/json 11 | - feat: make request with requests.request 12 | - feat: extract response status_code, headers field, field in json body 13 | - feat: assert equivalence for extracted field with expected value 14 | - feat: share public parameters in multiple apis of testcase 15 | - feat: parameters correlation in single testcase 16 | - feat: session sharing in single testcase 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pytest-requests 2 | 3 | [![LICENSE](https://img.shields.io/github/license/debugtalk/pytest-requests.svg)](https://github.com/debugtalk/pytest-requests/blob/master/LICENSE) [![travis-ci](https://travis-ci.org/debugtalk/pytest-requests.svg?branch=master)](https://travis-ci.org/debugtalk/pytest-requests) [![coveralls](https://coveralls.io/repos/github/debugtalk/pytest-requests/badge.svg?branch=master)](https://coveralls.io/github/debugtalk/pytest-requests?branch=master) 4 | 5 | HTTP(S) testing with pytest and requests. 6 | -------------------------------------------------------------------------------- /demo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/debugtalk/pytest-requests/c9a253d8a69e47033b7d51d14b6e6670f84633a4/demo/__init__.py -------------------------------------------------------------------------------- /demo/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/debugtalk/pytest-requests/c9a253d8a69e47033b7d51d14b6e6670f84633a4/demo/api/__init__.py -------------------------------------------------------------------------------- /demo/api/cookies.py: -------------------------------------------------------------------------------- 1 | from pytest_requests import HttpRequest 2 | 3 | 4 | class ApiHttpBinGetCookies(HttpRequest): 5 | 6 | method = HttpRequest.EnumHttpMethod.GET 7 | url = "http://httpbin.org/cookies" 8 | headers = {"accept": "application/json"} 9 | 10 | 11 | class ApiHttpBinGetSetCookies(HttpRequest): 12 | 13 | method = HttpRequest.EnumHttpMethod.GET 14 | url = "http://httpbin.org/cookies/set" 15 | headers = {"accept": "text/plain"} 16 | -------------------------------------------------------------------------------- /demo/api/html_form.py: -------------------------------------------------------------------------------- 1 | from pytest_requests import HttpRequest 2 | 3 | 4 | class ApiHttpBinPostHtmlForm(HttpRequest): 5 | 6 | method = HttpRequest.EnumHttpMethod.POST 7 | url = "http://httpbin.org/post" 8 | headers = { 9 | "Content-Type": "application/x-www-form-urlencoded" 10 | } 11 | body = "custname={custname}&custtel={custtel}&custemail=m%40test.com&size=small&topping=cheese&topping=mushroom&delivery=14%3A30&comments=hello+world" 12 | 13 | 14 | class ApiHttpBinPostJson(HttpRequest): 15 | 16 | method = HttpRequest.EnumHttpMethod.POST 17 | url = "http://httpbin.org/post" 18 | headers = { 19 | "Content-Type": "application/json", 20 | "accept": "application/json" 21 | } 22 | body = { 23 | "comments": "hello world", 24 | "custemail": "m@test.com", 25 | "custname": "{custname}", 26 | "custtel": "{custtel}", 27 | "delivery": "14:30", 28 | "size": "small", 29 | "topping": [ 30 | "cheese", 31 | "mushroom" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /demo/api/http_methods.py: -------------------------------------------------------------------------------- 1 | from pytest_requests import HttpRequest 2 | 3 | 4 | class ApiHttpbinGet(HttpRequest): 5 | 6 | method = HttpRequest.EnumHttpMethod.GET 7 | url = "http://httpbin.org/get" 8 | params = { 9 | "abc": "111", 10 | "de": "222" 11 | } 12 | headers = {"accept": "application/json"} 13 | 14 | 15 | class ApiHttpBinPost(HttpRequest): 16 | 17 | method = HttpRequest.EnumHttpMethod.POST 18 | url = "http://httpbin.org/post" 19 | headers = { 20 | "Content-Type": "application/json", 21 | "accept": "application/json" 22 | } 23 | body = {"abc": 123} 24 | -------------------------------------------------------------------------------- /demo/api/redirects.py: -------------------------------------------------------------------------------- 1 | from pytest_requests import HttpRequest 2 | 3 | 4 | class ApiHttpBinGetRedirect302(HttpRequest): 5 | 6 | method = HttpRequest.EnumHttpMethod.GET 7 | url = "http://httpbin.org/redirect-to" 8 | params = { 9 | "url": "https://debugtalk.com", 10 | "status_code": 302 11 | } 12 | -------------------------------------------------------------------------------- /demo/api/response_formats.py: -------------------------------------------------------------------------------- 1 | from pytest_requests import HttpRequest 2 | 3 | 4 | class ApiHttpBinGetJson(HttpRequest): 5 | 6 | method = HttpRequest.EnumHttpMethod.GET 7 | url = "https://httpbin.org/json" 8 | 9 | -------------------------------------------------------------------------------- /demo/test_config.py: -------------------------------------------------------------------------------- 1 | from pytest_requests import TestCase 2 | 3 | from demo.api.response_formats import ApiHttpBinGetJson 4 | 5 | 6 | class TestConfigRequest(TestCase): 7 | 8 | def test_config_verify(self): 9 | title = ApiHttpBinGetJson()\ 10 | .config_verify(False)\ 11 | .set_headers({"User-Agent": "pytest-requests"})\ 12 | .set_cookies({"username": "debugtalk"})\ 13 | .run()\ 14 | .assert_status_code(200)\ 15 | .assert_header("content-Type").equals("application/json")\ 16 | .assert_body("slideshow.slides[0].title").equals("Wake up to WonderWidgets!")\ 17 | .get_body("slideshow.slides[1].title") 18 | 19 | assert title == "Overview" 20 | -------------------------------------------------------------------------------- /demo/test_http_methods.py: -------------------------------------------------------------------------------- 1 | from demo.api.http_methods import * 2 | from pytest_requests import TestCase 3 | 4 | 5 | class TestHttpMethods(TestCase): 6 | 7 | def test_get(self): 8 | ApiHttpbinGet().run()\ 9 | .assert_status_code(200)\ 10 | .assert_("status_code").less_than(300)\ 11 | .assert_header("server").equals("nginx")\ 12 | .assert_body("url").equals("https://httpbin.org/get?abc=111&de=222")\ 13 | .assert_body("args").equals({"abc": "111", "de": "222"})\ 14 | .assert_body("headers.Accept").equals('application/json') 15 | 16 | def test_get_with_querystring(self): 17 | ApiHttpbinGet()\ 18 | .set_querystring({"abc": 123, "xyz": 456})\ 19 | .run()\ 20 | .assert_status_code(200)\ 21 | .assert_header("server").equals("nginx")\ 22 | .assert_body("url").equals("https://httpbin.org/get?abc=123&de=222&xyz=456")\ 23 | .assert_body("headers.Accept").equals('application/json')\ 24 | .assert_body("args").equals({"abc": "123", "de": "222", "xyz": "456"}) 25 | 26 | def test_post_json(self): 27 | ApiHttpBinPost() \ 28 | .set_body({"abc": 456})\ 29 | .run()\ 30 | .assert_status_code(200)\ 31 | .assert_header("server").equals("nginx")\ 32 | .assert_header("content-Type").equals("application/json")\ 33 | .assert_body("url").equals("https://httpbin.org/post")\ 34 | .assert_body("headers.Accept").equals('application/json')\ 35 | .assert_body('headers."Content-Type"').equals('application/json')\ 36 | .assert_body("json.abc").equals(456) 37 | 38 | headers = { 39 | "User-Agent": "pytest-requests", 40 | "content-type": "application/json" 41 | } 42 | ApiHttpBinPost()\ 43 | .set_headers(headers)\ 44 | .set_body({"abc": "123"})\ 45 | .run()\ 46 | .assert_status_code(200)\ 47 | .assert_header("server").equals("nginx") \ 48 | .assert_body("url").equals("https://httpbin.org/post")\ 49 | .assert_body("headers.Accept").equals('application/json')\ 50 | .assert_body('headers."Content-Type"').equals("application/json")\ 51 | .assert_body("json.abc").equals("123")\ 52 | .assert_body('headers."User-Agent"').equals("pytest-requests") 53 | 54 | def test_post_form_data(self): 55 | headers = { 56 | "User-Agent": "pytest-requests", 57 | "content-type": "application/x-www-form-urlencoded; charset=utf-8" 58 | } 59 | ApiHttpBinPost()\ 60 | .set_headers(headers)\ 61 | .set_body("abc=123")\ 62 | .run()\ 63 | .assert_status_code(200)\ 64 | .assert_header("server").equals("nginx")\ 65 | .assert_body("url").equals("https://httpbin.org/post")\ 66 | .assert_body("headers.Accept").equals('application/json')\ 67 | .assert_body('headers."Content-Type"').equals("application/x-www-form-urlencoded; charset=utf-8")\ 68 | .assert_body("form.abc").equals("123")\ 69 | .assert_body('headers."User-Agent"').equals("pytest-requests") 70 | 71 | def test_uniform_assert_method(self): 72 | ApiHttpBinPost()\ 73 | .set_body({"abc": 456})\ 74 | .run()\ 75 | .assert_("status_code").equals(200)\ 76 | .assert_("headers.server").equals("nginx")\ 77 | .assert_("headers.content-Type").equals("application/json")\ 78 | .assert_("body.url").equals("https://httpbin.org/post")\ 79 | .assert_("body.headers.Accept").equals('application/json')\ 80 | .assert_('body.headers."Content-Type"').equals('application/json')\ 81 | .assert_("body.json.abc").equals(456) 82 | -------------------------------------------------------------------------------- /demo/test_parameters.py: -------------------------------------------------------------------------------- 1 | from pytest_requests import TestCase 2 | from demo.api.http_methods import * 3 | from demo.api.cookies import * 4 | 5 | 6 | class TestParameters(TestCase): 7 | 8 | def test_share_parameters_between_steps(self): 9 | user_id = "adk129" 10 | ApiHttpbinGet()\ 11 | .set_querystring({"user_id": user_id})\ 12 | .run()\ 13 | .assert_status_code(200)\ 14 | .assert_header("server").equals("nginx")\ 15 | .assert_body("url").equals("https://httpbin.org/get?abc=111&de=222&user_id={}".format(user_id))\ 16 | .assert_body("headers.Accept").equals('application/json') 17 | 18 | ApiHttpBinPost()\ 19 | .set_body({"user_id": user_id})\ 20 | .run()\ 21 | .assert_status_code(200)\ 22 | .assert_header("server").equals("nginx")\ 23 | .assert_body("url").equals("https://httpbin.org/post")\ 24 | .assert_body("headers.Accept").equals('application/json')\ 25 | .assert_body("json.user_id").equals("adk129") 26 | 27 | def test_extract_parameter(self): 28 | api_run = ApiHttpbinGet().run() 29 | status_code = api_run.get_("status_code") 30 | assert status_code == 200 31 | 32 | server = api_run.get_header("server") 33 | assert server == "nginx" 34 | 35 | accept_type = api_run.get_body("headers.Accept") 36 | assert accept_type == 'application/json' 37 | 38 | def test_parameters_association(self): 39 | # step 1: get value 40 | freeform = ApiHttpBinGetCookies()\ 41 | .set_cookies({"freeform": "123"})\ 42 | .run()\ 43 | .get_body("cookies.freeform") 44 | assert freeform == "123" 45 | 46 | # step 2: use value as parameter 47 | ApiHttpBinPost()\ 48 | .set_body({"freeform": freeform})\ 49 | .run()\ 50 | .assert_status_code(200)\ 51 | .assert_header("server").equals("nginx")\ 52 | .assert_body("url").equals("https://httpbin.org/post")\ 53 | .assert_body("headers.Accept").equals('application/json')\ 54 | .assert_body("json.freeform").equals(freeform) 55 | 56 | -------------------------------------------------------------------------------- /demo/test_redirects.py: -------------------------------------------------------------------------------- 1 | from pytest_requests import TestCase 2 | from demo.api.redirects import * 3 | 4 | 5 | class TestRedirects(TestCase): 6 | 7 | def test_redirect_allow_redirects(self): 8 | ApiHttpBinGetRedirect302()\ 9 | .config_allow_redirects(False)\ 10 | .set_querystring({"status_code": 302})\ 11 | .run()\ 12 | .assert_status_code(302) 13 | -------------------------------------------------------------------------------- /demo/test_session.py: -------------------------------------------------------------------------------- 1 | from pytest_requests import TestCase 2 | from demo.api.html_form import * 3 | from demo.api.cookies import * 4 | from demo.api.http_methods import * 5 | 6 | 7 | class TestUpdatePostBody(TestCase): 8 | 9 | def run_test(self): 10 | session = self.create_session() 11 | 12 | kwargs = { 13 | "custname": "leo", 14 | "custtel": "18699999999" 15 | } 16 | form_data_body = self.parse_body(ApiHttpBinPostHtmlForm.body, kwargs) 17 | ApiHttpBinPostHtmlForm(session)\ 18 | .set_headers({"User-Agent": "pytest-requests"})\ 19 | .set_body(form_data_body)\ 20 | .run()\ 21 | .assert_status_code(200)\ 22 | .assert_header("Content-Type").equals("application/json")\ 23 | .assert_body("form.comments").equals("hello world")\ 24 | .assert_body("form.topping[0]").equals("cheese")\ 25 | .assert_body("form.custname").equals("leo")\ 26 | .assert_body("form.custtel").equals("18699999999") 27 | 28 | json_body = self.parse_body(ApiHttpBinPostJson.body, kwargs) 29 | ApiHttpBinPostJson(session)\ 30 | .set_headers({"User-Agent": "pytest-requests"})\ 31 | .set_body(json_body)\ 32 | .run()\ 33 | .assert_status_code(200)\ 34 | .assert_header("Content-Type").equals("application/json")\ 35 | .assert_body("json.comments").equals("hello world")\ 36 | .assert_body("json.topping[0]").equals("cheese")\ 37 | .assert_body("json.custname").equals("leo")\ 38 | .assert_body("json.custtel").equals("18699999999") 39 | 40 | 41 | class TestLoginStatus(TestCase): 42 | 43 | def run_test(self): 44 | session = self.create_session() 45 | 46 | # step1: login and get cookie 47 | ApiHttpBinGetSetCookies(session)\ 48 | .set_querystring({"freeform": "567"})\ 49 | .run() 50 | 51 | # step2: request another api, check cookie 52 | resp = ApiHttpBinPost(session)\ 53 | .set_body({"abc": 123})\ 54 | .run()\ 55 | .get_response_object() 56 | 57 | request_headers = resp.request.headers 58 | assert "freeform=567" in request_headers["Cookie"] 59 | 60 | 61 | class TestCookies(TestCase): 62 | 63 | def test_httpbin_setcookies(self): 64 | cookies = { 65 | "freeform1": "123", 66 | "freeform2": "456" 67 | } 68 | api_run = ApiHttpBinGetCookies().set_cookies(cookies).run() 69 | freeform1 = api_run.get_body("cookies.freeform1") 70 | freeform2 = api_run.get_body("cookies.freeform2") 71 | assert freeform1 == "123" 72 | assert freeform2 == "456" 73 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "main" 3 | description = "Atomic file writes." 4 | name = "atomicwrites" 5 | optional = false 6 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 7 | version = "1.3.0" 8 | 9 | [[package]] 10 | category = "main" 11 | description = "Classes Without Boilerplate" 12 | name = "attrs" 13 | optional = false 14 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 15 | version = "19.1.0" 16 | 17 | [[package]] 18 | category = "main" 19 | description = "Python package for providing Mozilla's CA Bundle." 20 | name = "certifi" 21 | optional = false 22 | python-versions = "*" 23 | version = "2019.6.16" 24 | 25 | [[package]] 26 | category = "main" 27 | description = "Universal encoding detector for Python 2 and 3" 28 | name = "chardet" 29 | optional = false 30 | python-versions = "*" 31 | version = "3.0.4" 32 | 33 | [[package]] 34 | category = "main" 35 | description = "Cross-platform colored terminal text." 36 | marker = "sys_platform == \"win32\"" 37 | name = "colorama" 38 | optional = false 39 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 40 | version = "0.4.1" 41 | 42 | [[package]] 43 | category = "dev" 44 | description = "Code coverage measurement for Python" 45 | name = "coverage" 46 | optional = false 47 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" 48 | version = "4.5.4" 49 | 50 | [[package]] 51 | category = "dev" 52 | description = "Show coverage stats online via coveralls.io" 53 | name = "coveralls" 54 | optional = false 55 | python-versions = "*" 56 | version = "1.8.2" 57 | 58 | [package.dependencies] 59 | coverage = ">=3.6,<5.0" 60 | docopt = ">=0.6.1" 61 | requests = ">=1.0.0" 62 | 63 | [[package]] 64 | category = "dev" 65 | description = "Pythonic argument parser, that will make you smile" 66 | name = "docopt" 67 | optional = false 68 | python-versions = "*" 69 | version = "0.6.2" 70 | 71 | [[package]] 72 | category = "main" 73 | description = "Internationalized Domain Names in Applications (IDNA)" 74 | name = "idna" 75 | optional = false 76 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 77 | version = "2.8" 78 | 79 | [[package]] 80 | category = "main" 81 | description = "Read metadata from Python packages" 82 | name = "importlib-metadata" 83 | optional = false 84 | python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3" 85 | version = "0.19" 86 | 87 | [package.dependencies] 88 | zipp = ">=0.5" 89 | 90 | [[package]] 91 | category = "main" 92 | description = "JSON Matching Expressions" 93 | name = "jmespath" 94 | optional = false 95 | python-versions = "*" 96 | version = "0.9.4" 97 | 98 | [[package]] 99 | category = "main" 100 | description = "More routines for operating on iterables, beyond itertools" 101 | name = "more-itertools" 102 | optional = false 103 | python-versions = ">=3.4" 104 | version = "7.2.0" 105 | 106 | [[package]] 107 | category = "main" 108 | description = "Core utilities for Python packages" 109 | name = "packaging" 110 | optional = false 111 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 112 | version = "19.1" 113 | 114 | [package.dependencies] 115 | attrs = "*" 116 | pyparsing = ">=2.0.2" 117 | six = "*" 118 | 119 | [[package]] 120 | category = "main" 121 | description = "Object-oriented filesystem paths" 122 | marker = "python_version < \"3.6\"" 123 | name = "pathlib2" 124 | optional = false 125 | python-versions = "*" 126 | version = "2.3.4" 127 | 128 | [package.dependencies] 129 | six = "*" 130 | 131 | [[package]] 132 | category = "main" 133 | description = "plugin and hook calling mechanisms for python" 134 | name = "pluggy" 135 | optional = false 136 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 137 | version = "0.12.0" 138 | 139 | [package.dependencies] 140 | importlib-metadata = ">=0.12" 141 | 142 | [[package]] 143 | category = "main" 144 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 145 | name = "py" 146 | optional = false 147 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 148 | version = "1.8.0" 149 | 150 | [[package]] 151 | category = "main" 152 | description = "Python parsing module" 153 | name = "pyparsing" 154 | optional = false 155 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 156 | version = "2.4.2" 157 | 158 | [[package]] 159 | category = "main" 160 | description = "pytest: simple powerful testing with Python" 161 | name = "pytest" 162 | optional = false 163 | python-versions = ">=3.5" 164 | version = "5.0.1" 165 | 166 | [package.dependencies] 167 | atomicwrites = ">=1.0" 168 | attrs = ">=17.4.0" 169 | colorama = "*" 170 | importlib-metadata = ">=0.12" 171 | more-itertools = ">=4.0.0" 172 | packaging = "*" 173 | pluggy = ">=0.12,<1.0" 174 | py = ">=1.5.0" 175 | wcwidth = "*" 176 | 177 | [package.dependencies.pathlib2] 178 | python = "<3.6" 179 | version = ">=2.2.0" 180 | 181 | [[package]] 182 | category = "main" 183 | description = "Python HTTP for Humans." 184 | name = "requests" 185 | optional = false 186 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 187 | version = "2.22.0" 188 | 189 | [package.dependencies] 190 | certifi = ">=2017.4.17" 191 | chardet = ">=3.0.2,<3.1.0" 192 | idna = ">=2.5,<2.9" 193 | urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" 194 | 195 | [[package]] 196 | category = "main" 197 | description = "Python 2 and 3 compatibility utilities" 198 | name = "six" 199 | optional = false 200 | python-versions = ">=2.6, !=3.0.*, !=3.1.*" 201 | version = "1.12.0" 202 | 203 | [[package]] 204 | category = "main" 205 | description = "HTTP library with thread-safe connection pooling, file post, and more." 206 | name = "urllib3" 207 | optional = false 208 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" 209 | version = "1.25.3" 210 | 211 | [[package]] 212 | category = "main" 213 | description = "Measures number of Terminal column cells of wide-character codes" 214 | name = "wcwidth" 215 | optional = false 216 | python-versions = "*" 217 | version = "0.1.7" 218 | 219 | [[package]] 220 | category = "main" 221 | description = "Backport of pathlib-compatible object wrapper for zip files" 222 | name = "zipp" 223 | optional = false 224 | python-versions = ">=2.7" 225 | version = "0.5.2" 226 | 227 | [metadata] 228 | content-hash = "a0005c4e72b29fca5e2699f4bd367f635a59c1642d2c5b3f5b8bb8275871925d" 229 | python-versions = "^3.5" 230 | 231 | [metadata.hashes] 232 | atomicwrites = ["03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", "75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"] 233 | attrs = ["69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", "f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"] 234 | certifi = ["046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", "945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695"] 235 | chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] 236 | colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"] 237 | coverage = ["08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", "0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", "141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", "19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", "23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", "245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", "331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", "386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", "3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", "60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", "63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", "6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", "6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", "7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", "826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", "93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", "9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", "af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", "bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", "bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", "c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", "dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", "df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", "e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", "e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", "e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", "eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", "eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", "ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", "efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", "fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", "ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"] 238 | coveralls = ["9bc5a1f92682eef59f688a8f280207190d9a6afb84cef8f567fa47631a784060", "fb51cddef4bc458de347274116df15d641a735d3f0a580a9472174e2e62f408c"] 239 | docopt = ["49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"] 240 | idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"] 241 | importlib-metadata = ["23d3d873e008a513952355379d93cbcab874c58f4f034ff657c7a87422fa64e8", "80d2de76188eabfbfcf27e6a37342c2827801e59c4cc14b0371c56fed43820e3"] 242 | jmespath = ["3720a4b1bd659dd2eecad0666459b9788813e032b83e7ba58578e48254e0a0e6", "bde2aef6f44302dfb30320115b17d030798de8c4110e28d5cf6cf91a7a31074c"] 243 | more-itertools = ["409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", "92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"] 244 | packaging = ["a7ac867b97fdc07ee80a8058fe4435ccd274ecc3b0ed61d852d7d53055528cf9", "c491ca87294da7cc01902edbe30a5bc6c4c28172b5138ab4e4aa1b9d7bfaeafe"] 245 | pathlib2 = ["2156525d6576d21c4dcaddfa427fae887ef89a7a9de5cbfe0728b3aafa78427e", "446014523bb9be5c28128c4d2a10ad6bb60769e78bd85658fe44a450674e0ef8"] 246 | pluggy = ["0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc", "b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c"] 247 | py = ["64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", "dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"] 248 | pyparsing = ["6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", "d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"] 249 | pytest = ["6ef6d06de77ce2961156013e9dff62f1b2688aa04d0dc244299fe7d67e09370d", "a736fed91c12681a7b34617c8fcefe39ea04599ca72c608751c31d89579a3f77"] 250 | requests = ["11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"] 251 | six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"] 252 | urllib3 = ["b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", "dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"] 253 | wcwidth = ["3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", "f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"] 254 | zipp = ["4970c3758f4e89a7857a973b1e2a5d75bcdc47794442f2e2dd4fe8e0466e809a", "8a5712cfd3bb4248015eb3b0b3c54a5f6ee3f2425963ef2a0125b8bc40aafaec"] 255 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pytest-requests" 3 | version = "0.1.0" 4 | description = "HTTP(S) testing with pytest and requests." 5 | authors = ["debugtalk "] 6 | license = "Apache-2.0" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.5" 10 | pytest = "^5.0" 11 | requests = "^2.22" 12 | jmespath = "^0.9.4" 13 | 14 | [tool.poetry.dev-dependencies] 15 | coverage = "^4.5" 16 | coveralls = "^1.8" 17 | 18 | [build-system] 19 | requires = ["poetry>=0.12"] 20 | build-backend = "poetry.masonry.api" 21 | -------------------------------------------------------------------------------- /pytest_requests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = "0.1.0" 3 | 4 | from pytest_requests.http import HttpRequest 5 | from pytest_requests.testcase import TestCase 6 | -------------------------------------------------------------------------------- /pytest_requests/exceptions.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | JSONDecodeError = json.JSONDecodeError 4 | 5 | """ failure type exceptions 6 | these exceptions will mark test as failure 7 | """ 8 | 9 | class MyBaseFailure(Exception): 10 | pass 11 | 12 | class ExtractFailure(MyBaseFailure): 13 | pass 14 | 15 | 16 | """ error type exceptions 17 | these exceptions will mark test as error 18 | """ 19 | 20 | class MyBaseError(Exception): 21 | pass 22 | 23 | class ParamsError(MyBaseError): 24 | pass 25 | -------------------------------------------------------------------------------- /pytest_requests/http/__init__.py: -------------------------------------------------------------------------------- 1 | from .request import HttpRequest -------------------------------------------------------------------------------- /pytest_requests/http/request.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | from typing import Any, Tuple 4 | 5 | import requests 6 | 7 | from pytest_requests.exceptions import ParamsError 8 | 9 | from .response import HttpResponse, ResponseObject 10 | 11 | 12 | class HttpRequest(object): 13 | 14 | class EnumHttpMethod(object): 15 | """ Enum HTTP method 16 | """ 17 | GET, HEAD, POST, PUT, OPTIONS, DELETE \ 18 | = ("GET", "HEAD", "POST", "PUT", "OPTIONS", "DELETE") 19 | 20 | def __init__(self, session=None): 21 | self.__session = session or requests.sessions.Session() 22 | self.__kwargs = { 23 | "params": copy.deepcopy(getattr(self, "params", {})), 24 | "headers": copy.deepcopy(getattr(self, "headers", {})), 25 | "cookies": copy.deepcopy(getattr(self, "cookies", {})) 26 | } 27 | 28 | def config_verify(self, is_verify: bool) -> "HttpRequest": 29 | """ config whether to verify the server’s TLS certificate 30 | """ 31 | self.__kwargs["verify"] = is_verify 32 | return self 33 | 34 | def config_timeout(self, timeout: float) -> "HttpRequest": 35 | """ config how many seconds to wait for the server to send data before giving up 36 | """ 37 | self.__kwargs["timeout"] = timeout 38 | return self 39 | 40 | def config_proxies(self, proxies: dict) -> "HttpRequest": 41 | """ config dictionary mapping protocol to the URL of the proxy. 42 | """ 43 | self.__kwargs["proxies"] = proxies 44 | return self 45 | 46 | def config_allow_redirects(self, is_allow_redirects: bool) -> "HttpRequest": 47 | """ config whether to enable GET/OPTIONS/POST/PUT/PATCH/DELETE/HEAD redirection. 48 | """ 49 | self.__kwargs["allow_redirects"] = is_allow_redirects 50 | return self 51 | 52 | def config_auth(self, auth: Tuple[str, str]) -> "HttpRequest": 53 | """ config Basic/Digest/Custom HTTP Auth, (username, password). 54 | """ 55 | self.__kwargs["auth"] = auth 56 | return self 57 | 58 | def set_querystring(self, params: dict) -> "HttpRequest": 59 | """ update request query params 60 | """ 61 | self.__kwargs["params"].update(params) 62 | return self 63 | 64 | def set_headers(self, headers: dict) -> "HttpRequest": 65 | """ update request headers 66 | """ 67 | self.__kwargs["headers"].update(headers) 68 | return self 69 | 70 | def set_cookies(self, cookies: dict) -> "HttpRequest": 71 | """ update request cookies 72 | """ 73 | self.__kwargs["cookies"].update(cookies) 74 | return self 75 | 76 | def set_body(self, body: Any) -> "HttpRequest": 77 | """ set request body 78 | """ 79 | self.__body = body 80 | return self 81 | 82 | def run(self) -> "HttpResponse": 83 | """ make HTTP(S) request 84 | """ 85 | try: 86 | resp_body = self.__body 87 | except AttributeError: 88 | resp_body = getattr(self, "body", None) 89 | 90 | if isinstance(resp_body, dict): 91 | if self.__kwargs["headers"] and \ 92 | self.__kwargs["headers"].get("content-type", "")\ 93 | .startswith("application/x-www-form-urlencoded"): 94 | self.__kwargs["data"] = json.dumps(resp_body) 95 | else: 96 | self.__kwargs["json"] = resp_body 97 | else: 98 | self.__kwargs["data"] = resp_body 99 | 100 | method = getattr(self, "method", "GET") 101 | try: 102 | url = getattr(self, "url") 103 | except AttributeError: 104 | raise ParamsError("url missing!") 105 | 106 | _resp_obj = self.__session.request( 107 | method, 108 | url, 109 | **self.__kwargs 110 | ) 111 | resp_obj = ResponseObject(_resp_obj) 112 | return HttpResponse(resp_obj) 113 | -------------------------------------------------------------------------------- /pytest_requests/http/response.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import jmespath 4 | import requests 5 | 6 | from pytest_requests import exceptions 7 | 8 | 9 | class Validator(object): 10 | 11 | def __init__(self, http_response: "HttpResponse", actual_value: Any): 12 | self.__http_response = http_response 13 | self.__actual_value = actual_value 14 | 15 | def equals(self, expected_value) -> "HttpResponse": 16 | actual_value = self.__actual_value 17 | assert actual_value == expected_value 18 | return self.__http_response 19 | 20 | def greater_than(self, expected_value) -> "HttpResponse": 21 | actual_value = self.__actual_value 22 | assert actual_value > expected_value 23 | return self.__http_response 24 | 25 | def less_than(self, expected_value) -> "HttpResponse": 26 | actual_value = self.__actual_value 27 | assert actual_value < expected_value 28 | return self.__http_response 29 | 30 | def greater_than_or_equals(self, expected_value) -> "HttpResponse": 31 | actual_value = self.__actual_value 32 | assert actual_value >= expected_value 33 | return self.__http_response 34 | 35 | def less_than_or_equals(self, expected_value) -> "HttpResponse": 36 | actual_value = self.__actual_value 37 | assert actual_value <= expected_value 38 | return self.__http_response 39 | 40 | 41 | class ResponseObject(object): 42 | 43 | def __init__(self, resp_obj): 44 | """ initialize with a requests.Response object 45 | 46 | Args: 47 | resp_obj (instance): requests.Response instance 48 | 49 | """ 50 | self.resp_obj = resp_obj 51 | 52 | def __getattr__(self, key): 53 | try: 54 | if key in ["json", "json()"]: 55 | value = self.resp_obj.json() 56 | elif key == "cookies": 57 | value = self.resp_obj.cookies.get_dict() 58 | else: 59 | value = getattr(self.resp_obj, key) 60 | 61 | self.__dict__[key] = value 62 | return value 63 | except AttributeError: 64 | err_msg = "ResponseObject does not have attribute: {}".format(key) 65 | # logger.log_error(err_msg) 66 | raise exceptions.ParamsError(err_msg) 67 | 68 | def get_header(self, field): 69 | """ extract header field. 70 | """ 71 | headers = self.headers 72 | if not field: 73 | # extract headers 74 | return headers 75 | 76 | try: 77 | return headers[field] 78 | except KeyError: 79 | err_msg = u"Failed to extract header! => headers.{}\n".format(field) 80 | err_msg += u"response headers: {}\n".format(headers) 81 | # logger.log_error(err_msg) 82 | raise exceptions.ExtractFailure(err_msg) 83 | 84 | def get_body(self, field): 85 | """ extract body field with jmespath. 86 | """ 87 | try: 88 | body = self.json 89 | except exceptions.JSONDecodeError: 90 | err_msg = u"Failed to extract body! => body.{}\n".format(field) 91 | err_msg += u"response body: {}\n".format(self.content) 92 | # logger.log_error(err_msg) 93 | raise exceptions.ExtractFailure(err_msg) 94 | 95 | if not field: 96 | # extract response body 97 | return body 98 | 99 | return jmespath.search(field, body) 100 | 101 | def extract(self, field): 102 | """ extract field from response object 103 | 104 | Args: 105 | field (str): string joined by delimiter. 106 | e.g. 107 | "status_code" 108 | "headers" 109 | "cookies" 110 | "content" 111 | "headers.content-type" 112 | "content.person.name.first_name" 113 | 114 | """ 115 | # string.split(sep=None, maxsplit=1) -> list of strings 116 | # e.g. "content.person.name" => ["content", "person.name"] 117 | try: 118 | top_query, sub_query = field.split('.', 1) 119 | except ValueError: 120 | top_query = field 121 | sub_query = None 122 | 123 | # status_code 124 | if top_query in ["status_code", "encoding", "ok", "reason", "url"]: 125 | if sub_query: 126 | # status_code.XX, ok.xyz 127 | err_msg = u"Failed to extract: {}\n".format(field) 128 | raise exceptions.ParamsError(err_msg) 129 | 130 | return getattr(self, top_query) 131 | 132 | # cookies 133 | elif top_query == "cookies": 134 | cookies = self.cookies 135 | if not sub_query: 136 | # extract cookies 137 | return cookies 138 | 139 | try: 140 | return cookies[sub_query] 141 | except KeyError: 142 | err_msg = u"Failed to extract cookie! => {}\n".format(field) 143 | err_msg += u"response cookies: {}\n".format(cookies) 144 | # logger.log_error(err_msg) 145 | raise exceptions.ExtractFailure(err_msg) 146 | 147 | # headers 148 | elif top_query == "headers": 149 | return self.get_header(sub_query) 150 | 151 | # response body 152 | elif top_query in ["body", "json()"]: 153 | return self.get_body(sub_query) 154 | 155 | 156 | class HttpResponse(object): 157 | 158 | def __init__(self, resp_obj: "ResponseObject"): 159 | self.__resp_obj = resp_obj 160 | 161 | def get_header(self, field: str): 162 | """ extract response header field. 163 | """ 164 | return self.__resp_obj.get_header(field) 165 | 166 | def get_body(self, field: str): 167 | """ extract response body field, field supports jmespath 168 | """ 169 | return self.__resp_obj.get_body(field) 170 | 171 | def get_(self, field: str): 172 | """ extract response field 173 | 174 | Args: 175 | field (str): response field 176 | e.g. status_code, headers.server, body.cookies.freeform 177 | 178 | """ 179 | return self.__resp_obj.extract(field) 180 | 181 | def get_response_object(self) -> "requests.Response": 182 | """ get response object. 183 | """ 184 | return self.__resp_obj.resp_obj 185 | 186 | def assert_status_code(self, expected_value: int) -> "HttpResponse": 187 | assert self.__resp_obj.extract("status_code") == expected_value 188 | return self 189 | 190 | def assert_(self, field: str) -> "Validator": 191 | """ Prepare universal validation. 192 | 193 | Args: 194 | field (str): any field in response 195 | status_code 196 | headers.Content-Type 197 | body.details[0].name 198 | 199 | """ 200 | actual_value = self.__resp_obj.extract(field) 201 | return Validator(self, actual_value) 202 | 203 | def assert_header(self, field: str) -> "Validator": 204 | """ Prepare header validation. 205 | 206 | Args: 207 | field (str): case insensitive string, 208 | content-type or Content-Type are both okay. 209 | 210 | """ 211 | actual_value = self.__resp_obj.get_header(field) 212 | return Validator(self, actual_value) 213 | 214 | def assert_body(self, field: str) -> "Validator": 215 | """ Prepare body validation, field supports jmespath. 216 | 217 | Args: 218 | field (str): jmespath string 219 | 220 | """ 221 | actual_value = self.__resp_obj.get_body(field) 222 | return Validator(self, actual_value) 223 | -------------------------------------------------------------------------------- /pytest_requests/testcase.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from pytest_requests.utils import parse_content 4 | 5 | 6 | class TestCase(object): 7 | 8 | def create_session(self): 9 | """ create new HTTP session 10 | """ 11 | return requests.sessions.Session() 12 | 13 | @staticmethod 14 | def parse_body(content, kwargs): 15 | return parse_content(content, kwargs) 16 | 17 | def run_test(self): 18 | """ run_test should be overrided. 19 | """ 20 | raise NotImplementedError 21 | -------------------------------------------------------------------------------- /pytest_requests/utils.py: -------------------------------------------------------------------------------- 1 | 2 | def parse_content(content, kwargs): 3 | 4 | if isinstance(content, dict): 5 | return { 6 | key: parse_content(value, kwargs) 7 | for key, value in content.items() 8 | } 9 | 10 | elif isinstance(content, list): 11 | return [ 12 | parse_content(item, kwargs) 13 | for item in content 14 | ] 15 | 16 | elif isinstance(content, str): 17 | return content.format(**kwargs) 18 | 19 | else: 20 | return content 21 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/debugtalk/pytest-requests/c9a253d8a69e47033b7d51d14b6e6670f84633a4/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from pytest_requests import utils 2 | 3 | 4 | def test_version(): 5 | from pytest_requests import __version__ 6 | assert isinstance(__version__, str) 7 | 8 | 9 | def test_parse_content(): 10 | kwargs = {"a": 1, "b": "xyz"} 11 | 12 | assert utils.parse_content("http://httpbin.org/", kwargs) == "http://httpbin.org/" 13 | assert utils.parse_content("POST", kwargs) == "POST" 14 | assert utils.parse_content(123, kwargs) == 123 15 | 16 | headers = { 17 | "a": "{a}", 18 | "User-Agent": "chrome" 19 | } 20 | assert utils.parse_content(headers, kwargs), {'a': '1', 'User-Agent': 'chrome'} 21 | 22 | data = "key=123&value={b}" 23 | assert utils.parse_content(data, kwargs) == "key=123&value=xyz" 24 | --------------------------------------------------------------------------------