├── .gitignore ├── README.md ├── actions ├── __init__.py ├── base_actions.py └── post_actions.py ├── configs ├── __init__.py └── config.py ├── e2e_tests ├── __init__.py └── post_crud_tests.py ├── locust_headless.conf ├── perf_tests ├── __init__.py └── post_perf_run.py ├── requirements.txt └── setup.cfg /.gitignore: -------------------------------------------------------------------------------- 1 | results 2 | .python-version 3 | .vscode 4 | .pytest_cache 5 | *.pyc 6 | perf_result* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This is a sample project to showcase how API functional test automation can be performed with Python & Pytest framework. 4 | 5 | The APIs defined for the functional tests are then reused to run performance & load tests using the locust framework. 6 | 7 | This project uses https://jsonplaceholder.typicode.com/ as the application under test. Please note it is a free to use fake Online REST API for testing and prototyping. 8 | 9 | For further information on pytest, take a look at https://docs.pytest.org/en/stable/ 10 | 11 | For further information on locust, take a look at https://locust.io/ 12 | 13 | # Project Setup 14 | 15 | This project is build with `python 3.8.5` 16 | 17 | It is recommended to use a python virtual environment to run python projects. You can set it up by following this: https://github.com/pyenv/pyenv-virtualenv 18 | 19 | Once your python environment is setup, you need to install the required packages by running 20 | 21 | ``` 22 | pip install -r requirements.txt 23 | ``` 24 | 25 | ## Functional Tests 26 | 27 | To run functional tests run 28 | 29 | ``` 30 | pytest 31 | ``` 32 | 33 | This runs all of the test files configured in `setup.cgf` 34 | 35 | The run also generates result artifacts under the `results` directory. These results can be viewed as an HTML page by running 36 | 37 | ``` 38 | allure serve results/ 39 | ``` 40 | 41 | ## Performance Tests 42 | 43 | ### In the GUI Mode 44 | 45 | To run the performance tests run in the UI 46 | 47 | ``` 48 | locust -f perf_tests/post_perf_run.py 49 | ``` 50 | 51 | The above script brings up the locust server which can be accessed at http://localhost:8089 52 | 53 | We need to provide the below mentioned inputs for the performance tests to begin: 54 | 55 | `Number of total users to simulate 1` 56 | 57 | `Spawn rate (users spawned/second) 1` 58 | 59 | `Host https://jsonplaceholder.typicode.com/` 60 | 61 | The test can be stopped using the stop button in the webpage. 62 | 63 | ### In Headless Mode 64 | 65 | To run the performance run in a headless mode (mostly for CI execution) run 66 | 67 | ``` 68 | locust --config=locust_headless.conf 69 | ``` 70 | 71 | This runs the tests as defined in `locust_headless.conf` config file 72 | 73 | The results will be generated in a csv format `perf_result_*.csv` 74 | 75 | ## Framework Organization 76 | 77 | ``` 78 | ├── README.md 79 | ├── actions 80 | │ ├── base_actions.py - Base API actions for all other actions file 81 | │ └── post_actions.py - API actions related to posts feature 82 | ├── configs 83 | │ ├── config.py - Environment configs 84 | ├── e2e_tests 85 | │ ├── post_crud_tests.py - e2e functional test file 86 | ├── locust_headless.conf - locust headless execution configs 87 | ├── perf_tests 88 | │ ├── post_perf_run.py - Performance test file 89 | ├── requirements.txt - Required python libraries 90 | ├── results - e2e results directory 91 | └── setup.cfg - Pytest config 92 | ``` 93 | -------------------------------------------------------------------------------- /actions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalsgit/pytest-locust-api-automation/a5f9e1a70418ce50fe1d9e29e8f0b6fecfe5d966/actions/__init__.py -------------------------------------------------------------------------------- /actions/base_actions.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import allure 3 | from configs.config import get_environment 4 | 5 | 6 | env = get_environment() 7 | host_url = env.get("host_url") 8 | 9 | 10 | def get(client, path, **kwargs): 11 | """Http GET method""" 12 | result = client.get(host_url + path, **kwargs) 13 | if client == requests: 14 | save_request_details(result.request) 15 | save_response_details(result) 16 | return result 17 | 18 | 19 | def post(client, path, **kwargs): 20 | """Http POST method""" 21 | result = client.post(host_url + path, **kwargs) 22 | if client == requests: 23 | save_request_details(result.request) 24 | save_response_details(result) 25 | return result 26 | 27 | 28 | def put(client, path, **kwargs): 29 | """Http PUT method""" 30 | result = client.put(host_url + path, **kwargs) 31 | if client == requests: 32 | save_request_details(result.request) 33 | save_response_details(result) 34 | return result 35 | 36 | 37 | def delete(client, path, **kwargs): 38 | """Http DELETE method""" 39 | result = client.delete(host_url + path, **kwargs) 40 | if client == requests: 41 | save_request_details(result.request) 42 | save_response_details(result) 43 | return result 44 | 45 | 46 | def patch(client, path, **kwargs): 47 | """Http PATCH method""" 48 | result = client.patch(host_url + path, **kwargs) 49 | if client == requests: 50 | save_request_details(result.request) 51 | save_response_details(result) 52 | return result 53 | 54 | 55 | def save_request_details(request): 56 | """Attach request details to test report""" 57 | allure.attach( 58 | "\n{}\n{}\n\n{}\n\n{}\n".format( 59 | "-----------Request----------->", 60 | request.method + " " + request.url, 61 | "\n".join("{}: {}".format(k, v) for k, v in request.headers.items()), 62 | request.body, 63 | ), 64 | "Request details", 65 | ) 66 | 67 | 68 | def save_response_details(response): 69 | """Attach response details to test report""" 70 | allure.attach( 71 | "\n{}\n{}\n\n{}\n\n{}\n".format( 72 | "<-----------Response-----------", 73 | "Status code:" + str(response.status_code), 74 | "\n".join("{}: {}".format(k, v) for k, v in response.headers.items()), 75 | response.text, 76 | ), 77 | "Response details", 78 | ) -------------------------------------------------------------------------------- /actions/post_actions.py: -------------------------------------------------------------------------------- 1 | import allure 2 | import json 3 | from actions import base_actions 4 | import requests 5 | from faker import Faker 6 | 7 | fake = Faker() 8 | 9 | 10 | @allure.step("Get all posts") 11 | def get_posts(client=requests, **kwargs): 12 | return base_actions.get(client, "posts", **kwargs) 13 | 14 | 15 | @allure.step("Create posts") 16 | def create_posts(title, post_body, user_id, client=requests, **kwargs): 17 | body = f"""{{"title": "{title}", "body": "{post_body}", "userId": {user_id}}}""" 18 | return base_actions.post(client, "posts/", data=body) 19 | 20 | 21 | @allure.step("Get a post") 22 | def get_post(post_id, client=requests, **kwargs): 23 | return base_actions.get(client, "posts/" + str(post_id)) 24 | 25 | 26 | @allure.step("Delete a post") 27 | def delete_post(post_id, client=requests, **kwargs): 28 | return base_actions.delete(client, "posts/" + str(post_id)) 29 | -------------------------------------------------------------------------------- /configs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalsgit/pytest-locust-api-automation/a5f9e1a70418ce50fe1d9e29e8f0b6fecfe5d966/configs/__init__.py -------------------------------------------------------------------------------- /configs/config.py: -------------------------------------------------------------------------------- 1 | from environs import Env 2 | 3 | env = Env() 4 | env.read_env() 5 | 6 | environments = { 7 | "local": { 8 | "host_url": "https://jsonplaceholder.typicode.com/", 9 | }, 10 | "staging": { 11 | "host_url": "https://jsonplaceholder.typicode.com/", 12 | }, 13 | "production": { 14 | "host_url": "https://jsonplaceholder.typicode.com/", 15 | }, 16 | } 17 | 18 | 19 | def get_environment(): 20 | """Retrieve environment variables""" 21 | app_env = environments.get(env("app_env", "local")) 22 | print(f"""Tests will be run against {app_env} environment""") 23 | return app_env 24 | -------------------------------------------------------------------------------- /e2e_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalsgit/pytest-locust-api-automation/a5f9e1a70418ce50fe1d9e29e8f0b6fecfe5d966/e2e_tests/__init__.py -------------------------------------------------------------------------------- /e2e_tests/post_crud_tests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import allure 3 | from actions import post_actions as post 4 | from faker import Faker 5 | 6 | fake = Faker() 7 | 8 | 9 | @allure.description("User should be able to get all posts") 10 | def test_get_all_posts(): 11 | result = post.get_posts() 12 | assert result.status_code == 200 13 | assert result.json()[0]["userId"] is not None 14 | assert result.json()[0]["id"] is not None 15 | assert result.json()[0]["title"] is not None 16 | assert result.json()[0]["body"] is not None 17 | 18 | 19 | @allure.description("User should be able to create and delete posts") 20 | def test_create_and_delete_posts(): 21 | user_id = 1 22 | post_title = fake.name() 23 | post_body = fake.sentence() 24 | 25 | # Create a new post 26 | result = post.create_posts(post_title, post_body, user_id) 27 | assert result.status_code == 201 28 | post_id = result.json()["id"] 29 | assert post_id is not None 30 | 31 | # Delete the created post 32 | result = post.delete_post(post_id) 33 | assert result.status_code == 200 34 | -------------------------------------------------------------------------------- /locust_headless.conf: -------------------------------------------------------------------------------- 1 | locustfile = perf_tests/post_perf_run.py 2 | headless= true 3 | host = https://jsonplaceholder.typicode.com/ 4 | users = 1 5 | hatch-rate = 1 6 | run-time = 5s 7 | csv=perf_result 8 | -------------------------------------------------------------------------------- /perf_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalsgit/pytest-locust-api-automation/a5f9e1a70418ce50fe1d9e29e8f0b6fecfe5d966/perf_tests/__init__.py -------------------------------------------------------------------------------- /perf_tests/post_perf_run.py: -------------------------------------------------------------------------------- 1 | from locust import HttpUser, between, task 2 | 3 | from configs.config import get_environment 4 | from actions import post_actions as posts 5 | from faker import Faker 6 | 7 | fake = Faker() 8 | 9 | 10 | class PostPerformanceTests(HttpUser): 11 | 12 | wait_time = between(0.3, 0.8) 13 | 14 | @task 15 | def get_all_posts(self): 16 | posts.get_posts(client=self.client, catch_response=True) 17 | 18 | @task 19 | def create_and_delete_post(self): 20 | user_id = 1 21 | post_title = fake.name() 22 | post_body = fake.sentence() 23 | 24 | # Create post 25 | with posts.create_posts( 26 | post_title, post_body, user_id, client=self.client, catch_response=True 27 | ) as response: 28 | self.post_id = response.json()["id"] 29 | print(self.post_id) 30 | 31 | # Delete post 32 | posts.delete_post(self.post_id, client=self.client, catch_response=True) 33 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | allure-pytest==2.8.18 2 | black==20.8b1 3 | Faker==4.1.3 4 | locust==1.2.3 5 | pytest==6.0.1 6 | requests==2.24.0 7 | environs==8.0.0 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | python_files = tests.py test_*.py *_tests.py 3 | addopts = -s --alluredir=results 4 | --------------------------------------------------------------------------------