我有一个简单的 FastAPI 应用程序,我正在尝试使用
pytest
为其创建测试。
我的目标是测试应用程序在出现不同错误时的行为方式。
我的应用程序中有一个简单的健康检查路线:
from fastapi import APIRouter
router = APIRouter()
@router.get("/health")
async def health():
return "It's working ✨"
现在在我的 pytest 模块中,我正在尝试修补上述函数,以便它引发不同的错误。 我正在使用
unittest.mock
但我的行为非常奇怪。
import pytest
from unittest import mock
from fastapi import HTTPException
from starlette.testclient import TestClient
import app.api.health
from app.main import app # this is my application (FastAPI instance) with the `router` attached
@pytest.fixture()
def client():
with TestClient(app) as test_client:
yield test_client
def test_simple(client):
def mock_health_function():
raise HTTPException(status_code=400, detail='gibberish')
with mock.patch('app.api.health.health', mock_health_function):
response = client.get(HEALTHCHECK_PATH)
with pytest.raises(HTTPException): # this check passes successfully - my exception is raised
app.api.health.health()
assert response.status_code != 200 # this check does not pass. The original function was called as if nothing was patched
尽管测试内部调用了完全相同的函数,但当我到达端点时,API 测试客户端仍然调用原始函数。
为什么测试中不直接调用函数时
mock.patch
无法正常工作?
或者也许我应该以不同的方式解决我的问题?
您可以使用
monkeypatch
夹具来修补您的功能。
首先拉出你要修补的代码部分:
from fastapi import FastAPI
app = FastAPI()
def response():
return "It's working ✨"
@app.get("/health")
async def health():
return response()
然后在你的测试中使用monkeypatch
import pytest
from fastapi import HTTPException
from starlette.testclient import TestClient
from app import main
def mocked_response():
raise HTTPException(status_code=400, detail='gibberish')
@pytest.fixture()
def client():
from app.main import app
with TestClient(app) as test_client:
yield test_client
def test_simple(client, monkeypatch):
monkeypatch.setattr(main, "response", mocked_response)
resp = client.get("/health")
assert resp.status_code == 400
assert resp.json()["detail"] == "gibberish"
另一种方法是使用 Dependency 和 dependency_overrides。 这可能不适用于所有场景,但对于您给定的用例来说它确实有效。
from fastapi import FastAPI, Depends
app = FastAPI()
def response():
return "It's working ✨"
@app.get("/health")
async def health(resp=Depends(response)):
return resp
在您的测试客户端中,您现在可以像这样覆盖依赖项:
import pytest
from fastapi import HTTPException
from starlette.testclient import TestClient
from app.main import response
def mocked_response():
raise HTTPException(status_code=400, detail='gibberish')
@pytest.fixture()
def client():
from app.main import app
app.dependency_overrides[response] = mocked_response
with TestClient(app) as test_client:
yield test_client
def test_simple(client):
resp = client.get("/health")
assert resp.status_code == 400
assert resp.json()["detail"] == "gibberish"
如果您需要向响应函数添加参数,您可以使用闭包模式
def response_closure():
def response(arg):
return arg
return response
@app.get("/health")
async def health(resp=Depends(response_closure)):
return resp("It's working ✨")
import pytest
from unittest import mock
from fastapi import HTTPException
from starlette.testclient import TestClient
import app.api.health
from app.main import app # this is my application (FastAPI instance) with the `router` attached
@pytest.fixture()
def client():
with TestClient(app) as test_client:
yield test_client
def test_simple(client):
def mock_health_function():
raise HTTPException(status_code=400, detail='gibberish')
with mock.patch('app.api.health.health', mock_health_function):
response = client.get(HEALTHCHECK_PATH)
with pytest.raises(HTTPException): # this check passes successfully - my exception is raised
app.api.health.health()
assert response.status_code != 200 # this check does not pass. The original function was called as if nothing was patched
您正在使用 with 语句将补丁用作上下文管理器,尝试将断言语句放入 with 块中,如上所示,我猜它应该会通过。
让我知道这是否有效!