unconsistent behavior between uvicorn and pytest
Hello,
In a server.py, I have a simple health check
# in server.py
from datetime import datetime, timezone
from blacksheep import Application, get, ok
app = Application()
@get("/health")
async def health():
return ok(f"{datetime.now(timezone.utc).isoformat()}")
and reusing the TestClient from https://www.neoteroi.dev/blacksheep/testing/#using-the-testclient-with-pytest, I have a simple test
# in test_api.py
import pytest
from blacksheep.testing import TestClient
@pytest.mark.asyncio(scope="session")
async def test_health_check(test_client: TestClient) -> None:
response = await test_client.get("/health")
assert response is not None
assert response.status is 200
print(response.headers)
assert response.has_header(b'content-type')
Bug
When running with pytest -sv, I'm getting an error because response.headers is empty.
$ PYTHONPATH=. pytest -sv
============================= test session starts ==============================
platform linux -- Python 3.11.2, pytest-8.2.2, pluggy-1.5.0 -- /home/debian/workspace/cde/cde-global/applications/ontology-server/.direnv/python-3.11.2/bin/python3
cachedir: .pytest_cache
rootdir: /home/debian/workspace/cde/cde-global/applications/ontology-server
plugins: asyncio-0.23.7
asyncio: mode=Mode.STRICT
collected 1 item
tests/test_api.py::test_health_check <Headers []>
FAILED
=================================== FAILURES ===================================
______________________________ test_health_check _______________________________
test_client = <blacksheep.testing.client.TestClient object at 0x7f2d176763d0>
@pytest.mark.asyncio(scope="session")
async def test_health_check(test_client: TestClient) -> None:
response = await test_client.get("/health")
assert response is not None
assert response.status is 200
print(response.headers)
> assert response.has_header(b'content-type')
E AssertionError: assert False
E + where False = <bound method Message.has_header of <Response 200>>(b'content-type')
E + where <bound method Message.has_header of <Response 200>> = <Response 200>.has_header
tests/test_api.py:13: AssertionError
=========================== short test summary info ============================
FAILED tests/test_api.py::test_health_check - AssertionError: assert False
============================== 1 failed in 0.04s ===============================
Expected
However, when I run the server with uvicorn server:app and I try with wget, I do get a content-type header.
$ wget -qS -O - http://127.0.0.1:8000/health
HTTP/1.1 200 OK
date: Sun, 23 Jun 2024 21:11:01 GMT
server: uvicorn
content-type: text/plain; charset=utf-8
content-length: 32
2024-06-23T21:11:02.151666+00:00
Has anyone an idea of the component/code adding this header after I return my response object and not present when testing ?
Thank you, Pierre
Hey, @ticapix, I found your question interesting. So, I've spent some time trying to figure out the root cause. Here's it...
Blacksheep sets Content-Type header when preparing a response for ASGI. By default, TestSimulator class doesn't set it. As a result you can see the header only during a regular HTTP request.
cdef void set_headers_for_response_content(Response message):
cdef Content content = message.content
if not content:
message._add_header(b'content-length', b'0')
return
message._add_header(b'content-type', content.type or b'application/octet-stream')
if should_use_chunked_encoding(content):
message._add_header(b'transfer-encoding', b'chunked')
else:
message._add_header(b'content-length', str(content.length).encode())
@ticapix take a look my PR #502
❯ pytest -svvv
====================================================================== test session starts ======================================================================
platform darwin -- Python 3.12.4, pytest-8.2.2, pluggy-1.5.0 -- /Users/User/.virtualenvs/blacksheep-issues/bin/python
cachedir: .pytest_cache
rootdir: /Users/User/workspace/neoteroi/BlackSheepIssues
plugins: asyncio-0.23.7
asyncio: mode=Mode.STRICT
collected 1 item
test_issue_501.py::test_health_check <Headers [(b'content-type', b'text/plain; charset=utf-8'), (b'content-length', b'32')]>
PASSED