Pytest fixtures in requests post function

Viewed 68

I'm trying to understand the example provided here and emulate it to work for the post method instead of get method.

All works well when I'm testing for response.status_code == 200. How do I test for more than one response status codes without repeating the code (like in here? I understand that one needs to use fixtures, how do I leverage them?

This is where I'm up to now.

app.py

from typing import Dict, Union
from urllib.error import HTTPError

import requests
import structlog

logger = structlog.get_logger()


def get_access_token(
    url: str, client_id: str, client_secret: str, grant_type: str
) -> Union[Dict[str, Union[str, int]], None]:
    try:
        r = requests.post(
            url,
            data={"grant_type": grant_type},
            auth=(client_id, client_secret),
            verify=True,
        )
        logger.info("Fetching token status code", status_code=r.status_code)
    except HTTPError as http_err:
        logger.error("HTTP error occurred", http_error=str(http_err))
    except Exception as err:
        logger.error("Other error occurred", exception=str(err))
    else:
        # The token expires in 10800 seconds
        return r.json()
test_app.py

# contents of test_app.py, a simple test for our API retrieval
from typing import Dict, Union

import pytest
import requests

from app import get_access_token


# custom class to be the mock return value of requests.post()
class MockResponse:
    status_code = 200
    raise_for_status = None

    @staticmethod
    def json() -> Dict[str, Union[str, int]]:
        return {
            'access_token': 'fake-access-token',
            'token_type': 'fake-token-type',
            'expires_in': 10800,
        }

@pytest.fixture
def mock_response(monkeypatch):
    """Requests.post() mocked to return json."""

    def mock_post(*args, **kwargs):
        return MockResponse()

    monkeypatch.setattr(requests, "post", mock_post)


# notice our test uses the custom fixture instead of monkeypatch directly
def test_get_access_token_success(mock_response) -> None:
    result = get_access_token(
        url="https://fakeurl",
        client_id="fake-client-id",
        client_secret="fake-client-secret",
        grant_type="fake-grant-type",
    )
    assert result['access_token'] == "fake-access-token"
    assert result['token_type'] == "fake-token-type"

rewrote app.py after @gold_cy's suggestion

from typing import Dict, Union

import requests
import structlog

logger = structlog.get_logger()


def get_access_token(
    url: str,
    client_id: str,
    client_secret: str,
    grant_type: str = 'client_credentials',
) -> Dict[str, Union[str, int]]:
    try:
        r = requests.post(
            url,
            data={"grant_type": grant_type},
            auth=(client_id, client_secret),
            verify=True,
        )
        logger.info("Fetching token status code", status_code=r.status_code)
        r.raise_for_status()
        if r.status_code == 200:
            # FIXME: The token expires in 10800 seconds
            return r.json()
    except requests.exceptions.HTTPError as err:
        logger.error("HTTP error", request=err.response.text)
        raise requests.HTTPError(err)
2 Answers

Without changing your code too much and with the assumption of testing status_code in mind, you can have your fixture return a function that you can provide status_code and raise_for_status into, in order to initialize your MockResponse class. However, looking at your token code you don't appear to utilize raise_for_status.

class MockResponse:
    def __init__(self, status_code, raise_for_status):
        self.status_code = status_code
        self.raise_for_status = raise_for_status

    @staticmethod
    def json() -> Dict[str, Union[str, int]]:
        return {
            'access_token': 'fake-access-token',
            'token_type': 'fake-token-type',
            'expires_in': 10800,
        }

@pytest.fixture
def mock_response(monkeypatch):
    """Requests.post() mocked to return json."""
    def wrapper(status_code, raise_for_status=None):
        def mock_post(*args, **kwargs):
            return MockResponse(status_code, raise_for_status)

        monkeypatch.setattr(requests, "post", mock_post)

    return wrapper


def test_some_random_status_code(mock_response) -> None:
    mock_response(301)

    result = get_access_token(
        url="https://fakeurl",
        client_id="fake-client-id",
        client_secret="fake-client-secret",
        grant_type="fake-grant-type",
    )

    assert result['access_token'] == "fake-access-token"

platform darwin -- Python 3.8.9, pytest-7.0.1, pluggy-1.0.0
rootdir: ***
plugins: asyncio-0.18.3, hypothesis-6.48.1, mock-3.7.0
asyncio: mode=strict
collected 1 item                                                                                    

tests/test_manager.py 2022-07-11 07:13.35 [info     ] Fetching token status code     status_code=301
.

========================================= 1 passed in 0.04s =========================================

Instead of response.status_code == 200 you can check just response.ok. If you want to check more statuses then you could check is it in valid range.

if response.status_code in range(200, 300):
    print("response status is 2xx")

if response.status_code in (200, 201):
    pass
Related