Requests using pytest

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

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

  1. Go to the ‘tests/responses’ directory.
  2. Create a subdirectory for the endpoint you want to mock or locate the existing one.
  3. Inside the endpoint directory, create a text file with this name format: HTTP_METHOD_query_parameters.txt

    HTTP_METHOD with the HTTP method (e.g., GET, POST). query_parameters with the query parameters, separated by underscores if multiple.

  4. Open the file and format it like this:

# status code: STATUS_CODE
# reason: REASON
BODY
  1. Replace:
    • STATUS_CODE with the desired HTTP status code (e.g., 200, 404).
    • REASON with the reason for the status code (e.g., “OK”, “Not Found”).
    • BODY with the response content to mock.
  2. 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&param2=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}")