pythonpytestfakeredis

Mocking with fakeredis+python results in HTTP 500


I have the following FastAPI module in python with the corresponding test, but am never able to get an HTTP 200 response from the await redis_service.set_key call I am making. Any suggestions on what I could be doing wrong?

THIS IS THE EXISTING CODE (I CAN REFACTOR THIS, BUT NEED WORKING TESTS BEFORE ATTEMPTING REFACTOR

from fastapi import FastAPI, HTTPException, Depends, Request
from .logs.log_messages import LOG_MESSAGES_INFO
from services.cache.service.cache_service import RedisService

# Define the service name for logging
service_name = "cache-service"

# Initialize the logger
logger = ServiceLogger(
    service_name, LOG_MESSAGES_INFO, os.getenv("LOG_FILE_PATH", "api.log")
)

# Create the FastAPI app
app = FastAPI(docs_url="/api", root_path="/plt/cache-service")

redis_url = os.getenv("REDIS_URL", "redis://127.0.0.1:6379")
redis_service = RedisService(redis_url)

# Define the endpoints
@app.post("/redis/set-key/")
async def set_key(request: Request, key_value: KeyValue, namespace: str):
    try:
        cr_id = request.headers.get("correlationid")
        logger.log("info", "MSREDI001", id=cr_id, method="set_key")
        
        # A BREAKPOINT HERE, DOESN'T ALLOW ME TO STEP INTO set_key
        # AND DIRECTLY RAISES AN EXCEPTION WITH 500 = <Response [500 Internal Server Error]>
        await redis_service.set_key(namespace, key_value.key, key_value.value, key_value.expire)
        return {"status": "success"}
    except Exception as e:
        logger.log("error", "MSREDE004", method="set_key", id=cr_id, error=str(e))
        raise HTTPException(status_code=500, detail=str(e))

The associated test:

import pytest
from unittest.mock import patch, MagicMock
from fastapi.testclient import TestClient
from pydantic import BaseModel
from services.cache.main import app
from fakeredis import aioredis

client = TestClient(app)

@pytest.fixture
def fake_redis():
    fake_redis_instance = aioredis.FakeRedis()
    return fake_redis_instance

@patch('services.cache.main.redis_service')  # Mock the RedisService
@patch('services.cache.main.logger')  # Mock the logger
def test_set_key_success(mock_logger, mock_redis_service, fake_redis):

    # Mock model for the key-value pair
    class KeyValue(BaseModel):
        key: str
        value: str
        expire: int

    mock_redis_service.set_key = MagicMock(side_effect=fake_redis.set)
    mock_logger.log = MagicMock()  # Mock the logger

    key_value = KeyValue(key="test_key", value="test_value", expire=3600)
    headers = {"correlationid": "4af44d6c-9e52-4929-9ff7-bbae3414e92f"}

    response = client.post("/redis/set-key/", json={
        "key": key_value.key,
        "value": key_value.value,
        "expire": key_value.expire
    }, headers=headers, params={"namespace": "test_namespace"})

    # Assert
    assert response.status_code == 200 # ALWAYS FAILS THIS ASSERT
    assert response.json() == {"status": "success"}

Solution

  • The direct use of aioredis.FakeRedis: aioredis.FakeRedis doesn’t properly integrate with the mock setup for redis_service.

    The redis_service.set_key is an asynchronous function, and mocking it synchronously with MagicMock can cause issues.

    In your case, you can redefine KeyValue, which might not match the schema used in the application.

    Updated Test:

    import pytest
    from unittest.mock import AsyncMock, patch
    from fastapi.testclient import TestClient
    from services.cache.main import app, redis_service
    from services.cache.main import KeyValue
    
    client = TestClient(app)
    
    @pytest.fixture
    def fake_redis():
        return AsyncMock()
    
    @patch('services.cache.main.redis_service')
    @patch('services.cache.main.logger')
    def test_set_key_success(mock_logger, mock_redis_service, fake_redis):
        mock_redis_service.set_key = fake_redis.set_key
        fake_redis.set_key.return_value = None
    
        key_value = {"key": "test_key", "value": "test_value", "expire": 3600}
        headers = {"correlationid": "4af44d6c-9e52-4929-9ff7-bbae3414e92f"}
        params = {"namespace": "test_namespace"}
    
        response = client.post("/redis/set-key/", json=key_value, headers=headers, params=params)
    
        assert response.status_code == 200
        assert response.json() == {"status": "success"}
    
        mock_logger.log.assert_called_with("info", "MSREDI001", id=headers["correlationid"], method="set_key")
        fake_redis.set_key.assert_called_with("test_namespace", "test_key", "test_value", 3600)
    
    
    @patch('services.cache.main.redis_service')
    @patch('services.cache.main.logger')
    def test_set_key_failure(mock_logger, mock_redis_service, fake_redis):
        mock_redis_service.set_key = fake_redis.set_key
        fake_redis.set_key.side_effect = Exception("Test Exception")
    
        key_value = {"key": "test_key", "value": "test_value", "expire": 3600}
        headers = {"correlationid": "4af44d6c-9e52-4929-9ff7-bbae3414e92f"}
        params = {"namespace": "test_namespace"}
    
        response = client.post("/redis/set-key/", json=key_value, headers=headers, params=params)
    
        assert response.status_code == 500
        assert response.json() == {"detail": "Test Exception"}
    
        mock_logger.log.assert_called_with(
            "error", "MSREDE004", method="set_key", id=headers["correlationid"], error="Test Exception"
        )
        fake_redis.set_key.assert_called_with("test_namespace", "test_key", "test_value", 3600)