├── .gitignore ├── README.md ├── answers ├── answers_01.py ├── answers_02.py ├── answers_03.py ├── answers_04.py ├── answers_05.py ├── answers_06.py └── test_data_zip.csv ├── examples ├── examples_01.py ├── examples_02.py ├── examples_03.py ├── examples_04.py ├── examples_05.py ├── examples_06.py └── test_data_users.csv ├── exercises ├── exercises_01.py ├── exercises_02.py ├── exercises_03.py ├── exercises_04.py ├── exercises_05.py └── exercises_06.py ├── python-requests-workshop.odp ├── python-requests-workshop.pdf ├── python-requests-workshop.pptx └── requirements.txt /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | 127 | # PyCharm 128 | .idea/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Workshop: Testing RESTful APIs in Python with requests 2 | ================== 3 | For those of you looking to gain some experience working with [requests](https://requests.readthedocs.io/en/master/), here are all the materials from a workshop I've created and delivered multiple times to good reviews. Feel free to use, share and adapt these materials as you see fit. 4 | 5 | Setting up your system 6 | --- 7 | 1) Make sure you have a working Python 3 installation. Get it from [here](https://www.python.org/downloads/) if you haven't got that already. 8 | 2) Download and unzip or clone this project 9 | 3) From the project root, run `pip install -r requirements.txt` to install the necessary dependencies 10 | 11 | What API is used for the exercises? 12 | --- 13 | The exercises use the [Zippopotam.us API](http://api.zippopotam.us/), so make sure that it's up and that you are allowed to access it from your machine. 14 | 15 | The same goes for the [SpaceX GraphQL API](https://api.spacex.land/graphql/) that is used in the sixth and final series of exercises. 16 | 17 | Slides 18 | --- 19 | The .pptx/.pdf/.odp file in the root folder contains all slides from the workshop. Again, feel free to use, share and adapt them to fit your own requirements. 20 | 21 | Comments? Saying thanks? 22 | --- 23 | Feel free to file an issue here or send me an email at bas@ontestautomation.com. 24 | 25 | I'd rather have you deliver the workshop instead... 26 | --- 27 | Sure, I'd love to. Again, send me an email and I'll be happy to discuss options. In house or at your conference, I'm sure we can work something out. -------------------------------------------------------------------------------- /answers/answers_01.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | # Exercise 1.1 5 | # Perform a GET request to http://api.zippopotam.us/us/90210 6 | # Check that the response status code equals 200 7 | def test_get_locations_for_us_90210_check_status_code_equals_200(): 8 | response = requests.get("http://api.zippopotam.us/us/90210") 9 | assert response.status_code == 200 10 | 11 | 12 | # Exercise 1.2 13 | # Perform a GET request to http://api.zippopotam.us/us/90210 14 | # Check that the value of the response header 'Content-Type' equals 'application/json' 15 | def test_get_locations_for_us_90210_check_content_type_equals_json(): 16 | response = requests.get("http://api.zippopotam.us/us/90210") 17 | assert response.headers["Content-Type"] == "application/json" 18 | 19 | 20 | # Exercise 1.3 21 | # Perform a GET request to http://api.zippopotam.us/us/90210 22 | # Check that the response body element 'country' has a value equal to 'United States' 23 | def test_get_locations_for_us_90210_check_country_equals_united_states(): 24 | response = requests.get("http://api.zippopotam.us/us/90210") 25 | response_body = response.json() 26 | assert response_body["country"] == "United States" 27 | 28 | 29 | # Exercise 1.4 30 | # Perform a GET request to http://api.zippopotam.us/us/90210 31 | # Check that the first 'place name' element in the list of places 32 | # has a value equal to 'Beverly Hills' 33 | def test_get_locations_for_us_90210_check_city_equals_beverly_hills(): 34 | response = requests.get("http://api.zippopotam.us/us/90210") 35 | response_body = response.json() 36 | assert response_body["places"][0]["place name"] == "Beverly Hills" 37 | 38 | 39 | # Exercise 1.5 40 | # Perform a GET request to http://api.zippopotam.us/us/90210 41 | # Check that the response body element 'places' has an array 42 | # value with a length of 1 (i.e., there's one place that corresponds 43 | # to the US zip code 90210) 44 | def test_get_locations_for_us_90210_check_one_place_is_returned(): 45 | response = requests.get("http://api.zippopotam.us/us/90210") 46 | response_body = response.json() 47 | assert len(response_body["places"]) == 1 48 | -------------------------------------------------------------------------------- /answers/answers_02.py: -------------------------------------------------------------------------------- 1 | import pytest, requests, csv 2 | 3 | # Exercise 2.1 4 | # Create a test data object test_data_zip 5 | # with three lines / test cases: 6 | # country code - zip code - place 7 | # us - 90210 - Beverly Hills 8 | # it - 50123 - Firenze 9 | # ca - Y1A - Whitehorse 10 | test_data_zip = [ 11 | ("us", "90210", "Beverly Hills"), 12 | ("it", "50123", "Firenze"), 13 | ("ca", "Y1A", "Whitehorse"), 14 | ] 15 | 16 | 17 | # Exercise 2.2 18 | # Write a parameterized test that retrieves user data using 19 | # a GET call to http://api.zippopotam.us// 20 | # and checks that the values for the 'place name' elements correspond 21 | # to those that are specified in the test data object 22 | @pytest.mark.parametrize("country_code, zip_code, expected_place", test_data_zip) 23 | def test_get_location_data_check_place_name(country_code, zip_code, expected_place): 24 | response = requests.get(f"http://api.zippopotam.us/{country_code}/{zip_code}") 25 | response_body = response.json() 26 | assert response_body["places"][0]["place name"] == expected_place 27 | 28 | 29 | # Exercise 2.3 30 | # Create the same test data as above, but now in a .csv file, for example: 31 | # us,90210,Beverly Hills 32 | # it,50123,Firenze 33 | # ca,Y1A,Whitehorse 34 | # Place this .csv file in the answers folder of the project 35 | 36 | 37 | # Exercise 2.4 38 | # Create a method read_data_from_csv() that reads the file from 2.3 line by line 39 | # and creates and returns a test data object from the data in the .csv file 40 | def read_data_from_csv(): 41 | test_data_zip_from_csv = [] 42 | with open("answers/test_data_zip.csv", newline="") as csvfile: 43 | data = csv.reader(csvfile, delimiter=",") 44 | for row in data: 45 | test_data_zip_from_csv.append(row) 46 | return test_data_zip_from_csv 47 | 48 | 49 | # Exercise 2.5 50 | # Change the data driven test from Exercise 2.2 so that it uses the test data 51 | # from the .csv file instead of the test data that was hard coded in this file 52 | @pytest.mark.parametrize("country_code, zip_code, expected_place", read_data_from_csv()) 53 | def test_get_location_data_check_place_name_with_data_from_csv( 54 | country_code, zip_code, expected_place 55 | ): 56 | response = requests.get(f"http://api.zippopotam.us/{country_code}/{zip_code}") 57 | response_body = response.json() 58 | assert response_body["places"][0]["place name"] == expected_place 59 | -------------------------------------------------------------------------------- /answers/answers_03.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | # Exercise 3.1 5 | # Create a function create_post() that returns an 6 | # object that follows this structure: 7 | # { 8 | # "title": "The title of my new post", 9 | # "body": "A very long string containing the body of my new post", 10 | # "userId": 1 11 | # } 12 | def create_post(): 13 | return { 14 | "title": "The title of my new post", 15 | "body": "A very long string containing the body of my new post", 16 | "userId": 1, 17 | } 18 | 19 | 20 | # Exercise 3.2 21 | # Write a test that POSTs the object created in 3.1 22 | # as JSON to https://jsonplaceholder.typicode.com/posts 23 | # and checks that the response status code is 201 24 | # and that the new post id returned by the API is an integer 25 | # Use the isinstance(variable, type) function for this (Google is your friend!) 26 | def test_send_post_check_status_code_is_200_and_id_is_integer(): 27 | response = requests.post( 28 | "https://jsonplaceholder.typicode.com/posts", json=create_post() 29 | ) 30 | assert response.status_code == 201 31 | assert isinstance(response.json()["id"], int) is True 32 | 33 | 34 | # Exercise 3.3 35 | # Create a function create_billpay_for(name) that takes 36 | # an argument of type string containing a name and returns 37 | # an object that follows this structure: 38 | # { 39 | # "name": , 40 | # "address": { 41 | # "street": "My street", 42 | # "city": "My city", 43 | # "state": "My state", 44 | # "zipCode": "90210" 45 | # }, 46 | # "phoneNumber": "0123456789", 47 | # "accountNumber": 12345 48 | # } 49 | def create_billpay_for(name): 50 | return { 51 | "name": name, 52 | "address": { 53 | "street": "My street", 54 | "city": "My city", 55 | "state": "My state", 56 | "zipCode": "90210", 57 | }, 58 | "phoneNumber": "0123456789", 59 | "accountNumber": 12345, 60 | } 61 | 62 | 63 | # Exercise 3.4 64 | # Write a test that POSTs the object created in 3.3 to 65 | # https://parabank.parasoft.com/parabank/services/bank/billpay?accountId=12345&amount=500 66 | # Supply a name of your own choice to the create_billpay_for() method 67 | # Make sure that the request header 'Accept' has value 'application/json' (Google ;) 68 | # Check that the response status code is 200 and 69 | # that the response body element 'payeeName' equals the name supplied to the method 70 | def test_payee_name_ends_up_in_response_body(): 71 | my_name = "John Smith" 72 | json_object = create_billpay_for(my_name) 73 | response = requests.post( 74 | "https://parabank.parasoft.com/parabank/services/bank/billpay?accountId=12345&amount=500", 75 | headers={"Accept": "application/json"}, 76 | json=json_object, 77 | ) 78 | assert response.status_code == 200 79 | assert response.json()["payeeName"] == my_name 80 | -------------------------------------------------------------------------------- /answers/answers_04.py: -------------------------------------------------------------------------------- 1 | from lxml import etree 2 | 3 | import requests 4 | 5 | 6 | # Exercise 4.1 7 | # Create a function create_xml_body_from_string() 8 | # that returns a docstring (with triple double quotes) 9 | # containing the following XML document: 10 | # 11 | # John Smith 12 | #
13 | # My street 14 | # My city 15 | # My state 16 | # 90210 17 | #
18 | # 0123456789 19 | # 12345 20 | #
21 | def create_xml_body_from_string(): 22 | return """ 23 | 24 | John Smith 25 |
26 | My street 27 | My city 28 | My state 29 | 90210 30 |
31 | 0123456789 32 | 12345 33 |
34 | """ 35 | 36 | 37 | # Exercise 4.2 38 | # Write a test that POSTs the object created in 4.1 39 | # to https://parabank.parasoft.com/parabank/services/bank/billpay?accountId=12345&amount=500 40 | # Set the request header 'Content-Type' to 'application/xml' 41 | # Then check that the response status code is 200 42 | # and that the value of the response header 'Content-Type' is also equal to 'application/xml' 43 | def test_send_xml_body_from_docstring_check_status_code_is_200_and_name_is_correct(): 44 | response = requests.post( 45 | "https://parabank.parasoft.com/parabank/services/bank/billpay?accountId=12345&amount=500", 46 | headers={"Content-Type": "application/xml"}, 47 | data=create_xml_body_from_string(), 48 | ) 49 | assert response.status_code == 200 50 | assert response.headers["Content-Type"] == "application/xml" 51 | 52 | 53 | # Exercise 4.3 54 | # Write a method create_xml_body_using_elementtree() that returns 55 | # the same request body as in Exercise 4.1, but now uses the 56 | # etree library from lxml (I've imported that for you already, it's available as 'etree') 57 | # Make your life a little easier by specifying all element values as strings 58 | def create_xml_body_using_elementtree(): 59 | payee = etree.Element("payee") 60 | name = etree.SubElement(payee, "name") 61 | name.text = "John Smith" 62 | address = etree.SubElement(payee, "address") 63 | street = etree.SubElement(address, "street") 64 | street.text = "My street" 65 | city = etree.SubElement(address, "city") 66 | city.text = "My city" 67 | state = etree.SubElement(address, "state") 68 | state.text = "My state" 69 | zip_code = etree.SubElement(address, "zipCode") 70 | zip_code.text = "90210" 71 | phone_number = etree.SubElement(payee, "phoneNumber") 72 | phone_number.text = "0123456789" 73 | account_number = etree.SubElement(payee, "accountNumber") 74 | account_number.text = "12345" 75 | return payee 76 | 77 | 78 | # Exercise 4.4 79 | # Repeat Exercise 4.2, but now use the XML document created in Exercise 4.3 80 | # Don't forget to convert the XML document to a string before sending it! 81 | def test_send_xml_body_from_elementtree_check_status_code_is_200_and_name_is_correct(): 82 | xml = create_xml_body_using_elementtree() 83 | xml_as_string = etree.tostring(xml) 84 | response = requests.post( 85 | "https://parabank.parasoft.com/parabank/services/bank/billpay?accountId=12345&amount=500", 86 | headers={"Content-Type": "application/xml"}, 87 | data=xml_as_string, 88 | ) 89 | print(response.request.body) 90 | assert response.status_code == 200 91 | assert response.headers["Content-Type"] == "application/xml" 92 | -------------------------------------------------------------------------------- /answers/answers_05.py: -------------------------------------------------------------------------------- 1 | from lxml import etree 2 | 3 | import requests 4 | 5 | 6 | # Exercise 5.1 7 | # Write a test that does the following: 8 | # Perform a GET to https://parabank.parasoft.com/parabank/services/bank/accounts/12345 9 | # Parse the response into an XML ElementTree 10 | # Check that the root element name is 'account' 11 | # Check that the root element has no attributes 12 | # Check that the root element has no text 13 | def test_check_root_of_xml_response(): 14 | response = requests.get( 15 | "https://parabank.parasoft.com/parabank/services/bank/accounts/12345" 16 | ) 17 | response_body_as_xml = etree.fromstring(response.content) 18 | xml_tree = etree.ElementTree(response_body_as_xml) 19 | root = xml_tree.getroot() 20 | assert root.tag == "account" 21 | assert len(root.attrib) == 0 22 | assert root.text is None 23 | 24 | 25 | # Exercise 5.2 26 | # Write a test that does the following 27 | # Perform a GET to https://parabank.parasoft.com/parabank/services/bank/accounts/12345 28 | # Parse the response into an XML ElementTree 29 | # Find the customerId element in the tree 30 | # Check that the text of the customerId element is '12212' 31 | def test_check_specific_element_of_xml_response(): 32 | response = requests.get( 33 | "https://parabank.parasoft.com/parabank/services/bank/accounts/12345" 34 | ) 35 | response_body_as_xml = etree.fromstring(response.content) 36 | xml_tree = etree.ElementTree(response_body_as_xml) 37 | first_name = xml_tree.find("customerId") 38 | assert first_name.text == "12212" 39 | 40 | 41 | # Exercise 5.3 42 | # Write a test that does the following 43 | # Perform a GET to https://parabank.parasoft.com/parabank/services/bank/customers/12212/accounts 44 | # Parse the response into an XML ElementTree 45 | # Find all 'account' elements in the entire XML document 46 | # Check that there are more than 5 of these 'account' elements 47 | def test_check_number_of_accounts_for_12212_greater_than_five(): 48 | response = requests.get( 49 | "https://parabank.parasoft.com/parabank/services/bank/customers/12212/accounts" 50 | ) 51 | response_body_as_xml = etree.fromstring(response.content) 52 | xml_tree = etree.ElementTree(response_body_as_xml) 53 | accounts = xml_tree.findall(".//account") 54 | assert len(accounts) > 5 55 | 56 | 57 | # Exercise 5.4 58 | # Repeat Exercise 5.3, but now check that: 59 | # - at least one of the accounts is of type 'SAVINGS' (Google!) 60 | # - there is no account that has a customerId that is not equal to 12212 61 | # (Use your creativity with the last one here... There is a solution, but I couldn't 62 | # find it on Google.) 63 | def test_use_xpath_for_more_sophisticated_checks(): 64 | response = requests.get( 65 | "https://parabank.parasoft.com/parabank/services/bank/customers/12212/accounts" 66 | ) 67 | response_body_as_xml = etree.fromstring(response.content) 68 | xml_tree = etree.ElementTree(response_body_as_xml) 69 | savings_accounts = xml_tree.findall(".//account/type[.='SAVINGS']") 70 | assert len(savings_accounts) > 1 71 | accounts_with_incorrect_customer_id = xml_tree.findall( 72 | ".//account/customerId[!.='12212']" 73 | ) 74 | assert len(accounts_with_incorrect_customer_id) == 0 75 | -------------------------------------------------------------------------------- /answers/answers_06.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import pytest 3 | 4 | 5 | # Exercise 6.1 6 | # Create a new GraphQL query as a String with value { company { name ceo coo } } 7 | # POST this object to https://api.spacex.land/graphql/ 8 | # Assert that the name of the CEO is Elon Musk 9 | # The name can be found using ['data']['company']['ceo'] 10 | def test_get_company_data_check_ceo_should_be_elon_musk(): 11 | 12 | response = requests.post( 13 | 'https://api.spacex.land/graphql/', 14 | json={'query': '{ company { name ceo coo } }'} 15 | ) 16 | response_body = response.json() 17 | assert response_body['data']['company']['ceo'] == 'Elon Musk' 18 | 19 | 20 | # Exercise 6.2 21 | # Create a test data source (a list of test data tuples) 22 | # containing the following test data: 23 | # ------------------------------------ 24 | # rocket id | rocket name | country 25 | # ------------------------------------ 26 | # falcon1 | Falcon 1 | Republic of the Marshall Islands 27 | # falconheavy | Falcon Heavy | United States 28 | # starship | Starship | United States 29 | test_data_rockets = [ 30 | ('falcon1', 'Falcon 1', 'Republic of the Marshall Islands'), 31 | ('falconheavy', 'Falcon Heavy', 'United States'), 32 | ('starship', 'Starship', 'United States') 33 | ] 34 | 35 | 36 | # Exercise 6.3 37 | # Write a test that POSTs the given parameterized GraphQL query to 38 | # https://api.spacex.land/graphql, together with the rocket id as 39 | # the value for the id variable, for all test cases in the test data source. 40 | # 41 | # Assert that the name of the rocket is equal to the value in the data source 42 | # Use ['data']['rocket']['name'] to extract it from the JSON response body. 43 | # 44 | # Assert that the country where the rocket was launched is equal to the value in the data source 45 | # Use ['data']['rocket']['country'] to extract it from the JSON response body. 46 | query_rocket_parameterized = """ 47 | query getRocketData($id: ID!) 48 | { 49 | rocket(id: $id) { 50 | name 51 | country 52 | } 53 | } 54 | """ 55 | 56 | 57 | @pytest.mark.parametrize('rocket_id, rocket_name, country', test_data_rockets) 58 | def test_get_rocket_data_check_name_and_country_should_equal_expected(rocket_id, rocket_name, country): 59 | 60 | response = requests.post( 61 | 'https://api.spacex.land/graphql/', 62 | json={ 63 | 'query': query_rocket_parameterized, 64 | 'variables': { 65 | 'id': rocket_id 66 | } 67 | } 68 | ) 69 | response_body = response.json() 70 | assert response_body['data']['rocket']['name'] == rocket_name 71 | assert response_body['data']['rocket']['country'] == country 72 | -------------------------------------------------------------------------------- /answers/test_data_zip.csv: -------------------------------------------------------------------------------- 1 | us,90210,Beverly Hills 2 | it,50123,Firenze 3 | ca,Y1A,Whitehorse -------------------------------------------------------------------------------- /examples/examples_01.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | def test_get_user_with_id_1_check_status_code_equals_200(): 5 | response = requests.get("https://jsonplaceholder.typicode.com/users/1") 6 | assert response.status_code == 200 7 | 8 | 9 | def test_get_user_with_id_1_check_content_type_equals_json(): 10 | response = requests.get("https://jsonplaceholder.typicode.com/users/1") 11 | assert response.headers["Content-Type"] == "application/json; charset=utf-8" 12 | 13 | 14 | def test_get_user_with_id_1_check_name_equals_leanne_graham(): 15 | response = requests.get("https://jsonplaceholder.typicode.com/users/1") 16 | response_body = response.json() 17 | assert response_body["name"] == "Leanne Graham" 18 | 19 | 20 | def test_get_user_with_id_1_check_company_name_equals_romaguera_crona(): 21 | response = requests.get("https://jsonplaceholder.typicode.com/users/1") 22 | response_body = response.json() 23 | assert response_body["company"]["name"] == "Romaguera-Crona" 24 | 25 | 26 | def test_get_all_users_check_number_of_users_equals_10(): 27 | response = requests.get("https://jsonplaceholder.typicode.com/users") 28 | response_body = response.json() 29 | assert len(response_body) == 10 30 | -------------------------------------------------------------------------------- /examples/examples_02.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import requests 3 | import csv 4 | 5 | test_data_users = [(1, "Leanne Graham"), (2, "Ervin Howell"), (3, "Clementine Bauch")] 6 | 7 | 8 | @pytest.mark.parametrize("userid, expected_name", test_data_users) 9 | def test_get_data_for_user_check_name(userid, expected_name): 10 | response = requests.get(f"https://jsonplaceholder.typicode.com/users/{userid}") 11 | response_body = response.json() 12 | assert response_body["name"] == expected_name 13 | 14 | 15 | def read_data_from_csv(): 16 | test_data_users_from_csv = [] 17 | with open("examples/test_data_users.csv", newline="") as csvfile: 18 | data = csv.reader(csvfile, delimiter=",") 19 | for row in data: 20 | test_data_users_from_csv.append(row) 21 | return test_data_users_from_csv 22 | 23 | 24 | @pytest.mark.parametrize("userid, expected_name", read_data_from_csv()) 25 | def test_get_location_data_check_place_name_with_data_from_csv(userid, expected_name): 26 | response = requests.get(f"https://jsonplaceholder.typicode.com/users/{userid}") 27 | response_body = response.json() 28 | assert response_body["name"] == expected_name 29 | -------------------------------------------------------------------------------- /examples/examples_03.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import random 3 | 4 | 5 | def create_new_post_object(): 6 | 7 | return { 8 | "name": "John Smith", 9 | "address": { 10 | "street": "Main Street", 11 | "number": random.randint(1000, 9999), 12 | "zipCode": 90210, 13 | "city": "Beverly Hills" 14 | } 15 | } 16 | 17 | 18 | def test_send_json_with_unique_number_check_status_code(): 19 | response = requests.post("https://postman-echo.com/post", json=create_new_post_object()) 20 | print(response.request.body) 21 | assert response.status_code == 200 22 | -------------------------------------------------------------------------------- /examples/examples_04.py: -------------------------------------------------------------------------------- 1 | from lxml import etree 2 | 3 | import requests 4 | import uuid 5 | 6 | unique_number = str(uuid.uuid4()) 7 | 8 | # 9 | # 10 | # 5b4832b4-da4c-48b2-8512-68fb49b69de1 11 | # John Smith 12 | # 0612345678 13 | # 0992345678 14 | # 15 | # 16 | 17 | 18 | def use_xml_string_block(): 19 | 20 | return ''' 21 | 22 | 23 | 5b4832b4-da4c-48b2-8512-68fb49b69de1 24 | John Smith 25 | 0612345678 26 | 0992345678 27 | 28 | 29 | ''' 30 | 31 | 32 | def create_xml_object(): 33 | users = etree.Element("users") 34 | user = etree.SubElement(users, "user") 35 | user_id = etree.SubElement(user, "id") 36 | user_id.text = unique_number 37 | name = etree.SubElement(user, "name") 38 | name.text = "John Smith" 39 | phone1 = etree.SubElement(user, "phone") 40 | phone1.set("type", "mobile") 41 | phone1.text = "0612345678" 42 | phone2 = etree.SubElement(user, "phone") 43 | phone2.set("type", "landline") 44 | phone2.text = "0992345678" 45 | 46 | return users 47 | 48 | 49 | def test_send_xml_using_xml_string_block(): 50 | response = requests.post("http://httpbin.org/anything", data=use_xml_string_block()) 51 | print(response.request.body) 52 | assert response.status_code == 200 53 | 54 | 55 | def test_send_xml_using_lxml_etree(): 56 | xml = create_xml_object() 57 | response = requests.post("http://httpbin.org/anything", data=etree.tostring(xml)) 58 | print(response.request.body) 59 | assert response.status_code == 200 60 | -------------------------------------------------------------------------------- /examples/examples_05.py: -------------------------------------------------------------------------------- 1 | from lxml import etree 2 | 3 | import requests 4 | 5 | 6 | def test_check_root_of_xml_response(): 7 | response = requests.get( 8 | "https://parabank.parasoft.com/parabank/services/bank/customers/12212" 9 | ) 10 | xml_response_element = etree.fromstring(response.content) 11 | xml_response_tree = etree.ElementTree(xml_response_element) 12 | root = xml_response_tree.getroot() 13 | assert root.tag == "customer" 14 | assert root.text is None 15 | 16 | 17 | def test_check_specific_element_of_xml_response(): 18 | response = requests.get( 19 | "https://parabank.parasoft.com/parabank/services/bank/customers/12212" 20 | ) 21 | xml_response_element = etree.fromstring(response.content) 22 | xml_response_tree = etree.ElementTree(xml_response_element) 23 | first_name = xml_response_tree.find("firstName") 24 | assert first_name.text == "John" 25 | assert len(first_name.attrib) == 0 26 | 27 | 28 | # https://docs.python.org/3/library/xml.etree.elementtree.html#elementtree-xpath 29 | def test_use_xpath_for_more_sophisticated_checks(): 30 | response = requests.get( 31 | "https://parabank.parasoft.com/parabank/services/bank/customers/12212" 32 | ) 33 | xml_response_element = etree.fromstring(response.content) 34 | xml_response_tree = etree.ElementTree(xml_response_element) 35 | address_children = xml_response_tree.findall(".//address/*") 36 | assert len(address_children) == 4 37 | -------------------------------------------------------------------------------- /examples/examples_06.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import pytest 3 | 4 | query_weather_in_amsterdam = """ 5 | { 6 | getCityByName(name: "Amsterdam") { 7 | weather { 8 | summary { 9 | title 10 | } 11 | } 12 | } 13 | } 14 | """ 15 | 16 | 17 | def test_get_weather_for_amsterdam_should_be_clear(): 18 | response = requests.post( 19 | "https://graphql-weather-api.herokuapp.com/", 20 | json={'query': query_weather_in_amsterdam} 21 | ) 22 | assert response.status_code == 200 23 | response_body = response.json() 24 | assert response_body['data']['getCityByName']['weather']['summary']['title'] == 'Clear' 25 | 26 | 27 | query_weather_parameterized = """ 28 | query getWeather($city: String!) 29 | { 30 | getCityByName(name: $city) { 31 | weather { 32 | summary { 33 | title 34 | } 35 | } 36 | } 37 | } 38 | """ 39 | 40 | test_data_weather = [ 41 | ('Amsterdam', 'Clear'), 42 | ('Berlin', 'Clear'), 43 | ('Sydney', 'Rain') 44 | ] 45 | 46 | 47 | @pytest.mark.parametrize('city_name, expected_weather', test_data_weather) 48 | def test_get_weather_for_city_should_be_as_expected(city_name, expected_weather): 49 | response = requests.post( 50 | "https://graphql-weather-api.herokuapp.com/", 51 | json={'query': query_weather_parameterized, 52 | 'variables': { 53 | 'city': city_name 54 | }} 55 | ) 56 | assert response.status_code == 200 57 | response_body = response.json() 58 | assert response_body['data']['getCityByName']['weather']['summary']['title'] == expected_weather 59 | -------------------------------------------------------------------------------- /examples/test_data_users.csv: -------------------------------------------------------------------------------- 1 | 1,Leanne Graham 2 | 2,Ervin Howell 3 | 3,Clementine Bauch -------------------------------------------------------------------------------- /exercises/exercises_01.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | # Exercise 1.1 5 | # Perform a GET request to http://api.zippopotam.us/us/90210 6 | # Check that the response status code equals 200 7 | 8 | 9 | # Exercise 1.2 10 | # Perform a GET request to http://api.zippopotam.us/us/90210 11 | # Check that the value of the response header 'Content-Type' equals 'application/json' 12 | 13 | 14 | # Exercise 1.3 15 | # Perform a GET request to http://api.zippopotam.us/us/90210 16 | # Check that the response body element 'country' has a value equal to 'United States' 17 | 18 | 19 | # Exercise 1.4 20 | # Perform a GET request to http://api.zippopotam.us/us/90210 21 | # Check that the first 'place name' element in the list of places 22 | # has a value equal to 'Beverly Hills' 23 | 24 | 25 | # Exercise 1.5 26 | # Perform a GET request to http://api.zippopotam.us/us/90210 27 | # Check that the response body element 'places' has an array 28 | # value with a length of 1 (i.e., there's one place that corresponds 29 | # to the US zip code 90210) 30 | -------------------------------------------------------------------------------- /exercises/exercises_02.py: -------------------------------------------------------------------------------- 1 | import pytest, requests, csv 2 | 3 | # Exercise 2.1 4 | # Create a test data object test_data_zip 5 | # with three lines / test cases: 6 | # country code - zip code - place 7 | # us - 90210 - Beverly Hills 8 | # it - 50123 - Firenze 9 | # ca - Y1A - Whitehorse 10 | 11 | 12 | # Exercise 2.2 13 | # Write a parameterized test that retrieves user data using 14 | # a GET call to http://api.zippopotam.us// 15 | # and checks that the values for the 'place name' elements correspond 16 | # to those that are specified in the test data object 17 | 18 | 19 | # Exercise 2.3 20 | # Create the same test data as above, but now in a .csv file, for example: 21 | # us,90210,Beverly Hills 22 | # it,50123,Firenze 23 | # ca,Y1A,Whitehorse 24 | # Place this .csv file in the answers folder of the project 25 | 26 | 27 | # Exercise 2.4 28 | # Create a method read_data_from_csv() that reads the file from 2.3 line by line 29 | # and creates and returns a test data object from the data in the .csv file 30 | 31 | 32 | # Exercise 2.5 33 | # Change the data driven test from Exercise 2.2 so that it uses the test data 34 | # from the .csv file instead of the test data that was hard coded in this file 35 | -------------------------------------------------------------------------------- /exercises/exercises_03.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | # Exercise 3.1 5 | # Create a function create_post() that returns an 6 | # object that follows this structure: 7 | # { 8 | # "title": "The title of my new post", 9 | # "body": "A very long string containing the body of my new post", 10 | # "userId": 1 11 | # } 12 | 13 | 14 | # Exercise 3.2 15 | # Write a test that POSTs the object created in 3.1 16 | # as JSON to https://jsonplaceholder.typicode.com/posts 17 | # and checks that the response status code is 201 18 | # and that the new post id returned by the API is an integer 19 | # Use the isinstance(variable, type) function for this (Google is your friend!) 20 | 21 | 22 | # Exercise 3.3 23 | # Create a function create_billpay_for(name) that takes 24 | # an argument of type string containing a name and returns 25 | # an object that follows this structure: 26 | # { 27 | # "name": , 28 | # "address": { 29 | # "street": "My street", 30 | # "city": "My city", 31 | # "state": "My state", 32 | # "zipCode": "90210" 33 | # }, 34 | # "phoneNumber": "0123456789", 35 | # "accountNumber": 12345 36 | # } 37 | 38 | 39 | # Exercise 3.4 40 | # Write a test that POSTs the object created in 3.3 to 41 | # https://parabank.parasoft.com/parabank/services/bank/billpay?accountId=12345&amount=500 42 | # Supply a name of your own choice to the create_billpay_for() method 43 | # Make sure that the request header 'Accept' has value 'application/json' (Google ;) 44 | # Check that the response status code is 200 and 45 | # that the response body element 'payeeName' equals the name supplied to the method 46 | -------------------------------------------------------------------------------- /exercises/exercises_04.py: -------------------------------------------------------------------------------- 1 | from lxml import etree 2 | 3 | import requests 4 | 5 | 6 | # Exercise 4.1 7 | # Create a function create_xml_body_from_string() 8 | # that returns a docstring (with triple double quotes) 9 | # containing the following XML document: 10 | # 11 | # John Smith 12 | #
13 | # My street 14 | # My city 15 | # My state 16 | # 90210 17 | #
18 | # 0123456789 19 | # 12345 20 | #
21 | 22 | 23 | # Exercise 4.2 24 | # Write a test that POSTs the object created in 4.1 25 | # to https://parabank.parasoft.com/parabank/services/bank/billpay?accountId=12345&amount=500 26 | # Set the request header 'Content-Type' to 'application/xml' 27 | # Then check that the response status code is 200 28 | # and that the value of the response header 'Content-Type' is also equal to 'application/xml' 29 | 30 | 31 | # Exercise 4.3 32 | # Write a method create_xml_body_using_elementtree() that returns 33 | # the same request body as in Exercise 4.1, but now uses the 34 | # ElementTree library (I've imported that for you already, it's available as 'et') 35 | # Make your life a little easier by specifying all element values as strings 36 | 37 | 38 | # Exercise 4.4 39 | # Repeat Exercise 4.2, but now use the XML document created in Exercise 4.3 40 | # Don't forget to convert the XML document to a string before sending it! 41 | -------------------------------------------------------------------------------- /exercises/exercises_05.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as et 2 | import requests 3 | 4 | 5 | # Exercise 5.1 6 | # Write a test that does the following: 7 | # Perform a GET to http://parabank.parasoft.com/parabank/services/bank/accounts/12345 8 | # Parse the response into an XML ElementTree 9 | # Check that the root element name is 'account' 10 | # Check that the root element has no attributes 11 | # Check that the root element has no text 12 | 13 | 14 | # Exercise 5.2 15 | # Write a test that does the following 16 | # Perform a GET to http://parabank.parasoft.com/parabank/services/bank/accounts/12345 17 | # Parse the response into an XML ElementTree 18 | # Find the customerId element in the tree 19 | # Check that the text of the customerId element is '12212' 20 | 21 | 22 | # Exercise 5.3 23 | # Write a test that does the following 24 | # Perform a GET to http://parabank.parasoft.com/parabank/services/bank/customers/12212/accounts 25 | # Parse the response into an XML ElementTree 26 | # Find all 'account' elements in the entire XML document 27 | # Check that there are more than 5 of these 'account' elements 28 | 29 | 30 | # Exercise 5.4 31 | # Repeat Exercise 5.3, but now check that: 32 | # - at least one of the accounts is of type 'SAVINGS' (Google!) 33 | # - there is no account that has a customerId that is not equal to 12212 34 | # (Use your creativity with the last one here... There is a solution, but I couldn't 35 | # find it on Google.) 36 | -------------------------------------------------------------------------------- /exercises/exercises_06.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import pytest 3 | 4 | 5 | # Exercise 6.1 6 | # Create a new GraphQL query as a String with value { company { name ceo coo } } 7 | # POST this object to https://api.spacex.land/graphql/ 8 | # Assert that the name of the CEO is Elon Musk 9 | # The name can be found using ['data']['company']['ceo'] 10 | 11 | 12 | # Exercise 6.2 13 | # Create a test data source (a list of test data tuples) 14 | # containing the following test data: 15 | # ------------------------------------ 16 | # rocket id | rocket name | country 17 | # ------------------------------------ 18 | # falcon1 | Falcon 1 | Republic of the Marshall Islands 19 | # falconheavy | Falcon Heavy | United States 20 | # starship | Starship | United States 21 | 22 | 23 | # Exercise 6.3 24 | # Write a test that POSTs the given parameterized GraphQL query to 25 | # https://api.spacex.land/graphql, together with the rocket id as 26 | # the value for the id variable, for all test cases in the test data source. 27 | # 28 | # Assert that the name of the rocket is equal to the value in the data source 29 | # Use ['data']['rocket']['name'] to extract it from the JSON response body. 30 | # 31 | # Assert that the country where the rocket was launched is equal to the value in the data source 32 | # Use ['data']['rocket']['country'] to extract it from the JSON response body. 33 | query_rocket_parameterized = """ 34 | query getRocketData($id: ID!) 35 | { 36 | rocket(id: $id) { 37 | name 38 | country 39 | } 40 | } 41 | """ 42 | -------------------------------------------------------------------------------- /python-requests-workshop.odp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basdijkstra/requests-workshop/50022aaedf56cb46ff77800d8a5f878d9f0569c8/python-requests-workshop.odp -------------------------------------------------------------------------------- /python-requests-workshop.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basdijkstra/requests-workshop/50022aaedf56cb46ff77800d8a5f878d9f0569c8/python-requests-workshop.pdf -------------------------------------------------------------------------------- /python-requests-workshop.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basdijkstra/requests-workshop/50022aaedf56cb46ff77800d8a5f878d9f0569c8/python-requests-workshop.pptx -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==7.1.2 2 | requests==2.32.0 3 | lxml==4.9.1 --------------------------------------------------------------------------------