Pytest and requests
This module provides a pytest fixture to mock HTTP requests. It provide custom responses based on response files.
Response files
Response files are organized in the ‘tests/responses’ directory.
Each response file is placed within a subdirectory named after the URI of the URL being tested. This ensures responses are matched accurately with test URLs.
File naming
Response files should be named based on the following convention:
HTTP_METHOD_query_parameters.txt
- HTTP_METHOD represents the HTTP method to be mocked (e.g., GET, POST).
- queryparameters represent query parameters, separated by underscores () if there are multiple.
Running Tests
When a test function is executed, it uses the http_request_fixture to intercept HTTP requests and
It return custom responses based on the corresponding response file.
Adding new response
- Go to the ‘tests/responses’ directory.
- Create a subdirectory for the endpoint you want to mock or locate the existing one.
-
Inside the endpoint directory, create a text file with this name format:
HTTP_METHOD_query_parameters.txtHTTP_METHOD with the HTTP method (e.g., GET, POST). query_parameters with the query parameters, separated by underscores if multiple.
-
Open the file and format it like this:
# status code: STATUS_CODE
# reason: REASON
BODY
- Replace:
STATUS_CODEwith the desired HTTP status code (e.g., 200, 404).REASONwith the reason for the status code (e.g., “OK”, “Not Found”).BODYwith the response content to mock.
- Save the file.
Example
import pytest
import requests
# Import the custom fixture from conftest.py
from conftest import http_request_fixture
def test_http_request(http_request_fixture):
# Define the URL to make an HTTP request
url = "https://example.com/path/to/resource?param1=value1¶m2=value2"
# Perform an HTTP request
response = requests.get(url)
# Check the response status code
assert response.status_code == 404 # This status code comes from the response file
# Check the response reason
assert response.reason == "Not Found"
# Check the response content (assuming a specific expected response in the file)
expected_response = "Response body content for 404 status code"
assert response.text.strip() == expected_response
Mock file path
|- tests
|- responses
|- path_to_resource
|- param1_value1_param2_value2.txt
Mock file content
# status_code: 404
# reason: Not Found
Response body content for 404 status code
Code
import os
from urllib.parse import parse_qs, urlparse
import pytest
import requests
from requests import Response
BASE_RESPONSE_PATH = os.path.join("tests", "responses")
def get_response_file(url: str) -> str:
"""
Get the response file path based on the URL.
"""
parsed_url = urlparse(url)
path_parts = parsed_url.path.strip("/").split("/")
method = parsed_url.scheme
query_params = parse_qs(parsed_url.query)
query_str = "_".join(
[
f"{param}_{value}"
for param, values in query_params.items()
for value in values
]
)
dir_name = "_".join(path_parts)
file_name = f"{method}_{query_str}.txt"
return os.path.join(BASE_RESPONSE_PATH, dir_name, file_name)
def load_response_content(response_file: str) -> list[str]:
"""
Load the content of a response file.
"""
with open(response_file) as file:
return file.readlines()
def extract_response_info(content_lines: list[str]) -> Response:
"""
Extract response information from response content lines
and create a Response object.
"""
status_code = 200
reason = ""
body = []
for line in content_lines:
line = line.strip()
if not line.startswith("# "):
body.append(line)
else:
line_parts = line.split(":", maxsplit=1)
line_key = line_parts[0]
line_value = line_parts[1].strip()
if "status_code" in line_key:
status_code = int(line_value)
elif "reason" in line_key:
reason = line_value
response = Response()
response.status_code = status_code
response.reason = reason
response._content = "\n".join(body).encode("utf-8")
return response
@pytest.fixture
def http_request_fixture(monkeypatch):
"""
Pytest fixture to mock HTTP requests and provide custom responses based on response files.
"""
def mock_request(*args, **kwargs):
url = args[1]
response_file = get_response_file(url)
content_lines = load_response_content(response_file)
return extract_response_info(content_lines)
monkeypatch.setattr(requests.sessions.Session, "request", mock_request)
Extracting Responses from actual API
import os
from urllib.parse import parse_qs, urlparse
from requests import Response
class ApiWrapper:
def __init__(self):
# Initialize your API client here
pass
def get_data(self) -> Response:
pass
def execute_and_save_responses(api: ApiWrapper):
responses = {}
# Define the methods to execute with arguments
methods_to_execute = {
api.get_data: None, # No arguments
# Add more methods with their respective arguments as needed
}
for method, args in methods_to_execute.items():
if args:
response = method(args) # Call the method with arguments
else:
response = method() # Call the method without arguments
# Convert the response data to a string for saving
file_content = prepare_file_content(response)
# Get the URL associated with the method (for response file path)
request_url = response.request.url
response_file_path = get_response_file(request_url)
save_response(response_file_path, file_content)
responses[method.__name__] = response_file_path
return responses
def get_response_file(url: str) -> str:
parsed_url = urlparse(url)
path_parts = parsed_url.path.strip("/").split("/")
method = parsed_url.scheme
query_params = parse_qs(parsed_url.query)
query_str = "_".join(
[
f"{param}_{value}"
for param, values in query_params.items()
for value in values
]
)
return os.path.join(
"tests", "responses", "_".join(path_parts), f"{method}_{query_str}.txt"
)
def prepare_file_content(response: Response):
return f"""# status_code: {response.status_code}
# reason: {response.reson}
{response.content}
"""
def save_response(file_path: str, content: str):
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "w") as file:
file.write(content)
if __name__ == "__main__":
api = ApiWrapper()
responses = execute_and_save_responses(api)
print(f"Responses saved successfully! {responses}")