18

I am using pytest to test a FastAPI endpoint that gets in input an image in binary format as in

@app.post("/analyse")
async def analyse(file: bytes = File(...)):

    image = Image.open(io.BytesIO(file)).convert("RGB")
    stats = process_image(image)
    return stats

After starting the server, I can manually test the endpoint successfully by running a call with requests

import requests
from requests_toolbelt.multipart.encoder import MultipartEncoder

url = "http://127.0.0.1:8000/analyse"

filename = "./example.jpg"
m = MultipartEncoder(
        fields={'file': ('filename', open(filename, 'rb'), 'image/jpeg')}
    )
r = requests.post(url, data=m, headers={'Content-Type': m.content_type}, timeout = 8000)
assert r.status_code == 200

However, setting up tests in a function of the form:

from fastapi.testclient import TestClient
from requests_toolbelt.multipart.encoder import MultipartEncoder
from app.server import app

client = TestClient(app)

def test_image_analysis():

    filename = "example.jpg"

    m = MultipartEncoder(
        fields={'file': ('filename', open(filename, 'rb'), 'image/jpeg')}
        )

    response = client.post("/analyse",
                           data=m,
                           headers={"Content-Type": "multipart/form-data"}
                           )

    assert response.status_code == 200

when running tests with python -m pytest, that gives me back a

>       assert response.status_code == 200
E       assert 400 == 200
E        +  where 400 = <Response [400]>.status_code

tests\test_server.py:22: AssertionError
-------------------------------------------------------- Captured log call --------------------------------------------------------- 
ERROR    fastapi:routing.py:133 Error getting request body: can't concat NoneType to bytes
===================================================== short test summary info ====================================================== 
FAILED tests/test_server.py::test_image_analysis - assert 400 == 200

what am I doing wrong?
What's the right way to write a test function test_image_analysis() using an image file?

Davide Fiocco
  • 5,350
  • 5
  • 35
  • 72

2 Answers2

32

You see a different behavior because requests and TestClient are not exactly same in every aspect as TestClient wraps requests. To dig deeper, refer to the source code: (FastAPI is using TestClient from starlette library, FYI)

https://github.com/encode/starlette/blob/master/starlette/testclient.py

To solve, you can get rid of MultipartEncoder because requests can accept file bytes and encode it by form-data format, with something like

# change it
r = requests.post(url, data=m, headers={'Content-Type': m.content_type}, timeout = 8000)

# to 
r = requests.post(url, files={"file": ("filename", open(filename, "rb"), "image/jpeg")})

and modifying the FastAPI test code:

# change
response = client.post("/analyse",
                       data=m,
                       headers={"Content-Type": "multipart/form-data"}
                       )
# to
response = client.post(
    "/analyse", files={"file": ("filename", open(filename, "rb"), "image/jpeg")}
)
Davide Fiocco
  • 5,350
  • 5
  • 35
  • 72
elprup
  • 1,960
  • 2
  • 18
  • 32
  • Note that other form data could be included, if needed, via `data=`, which accepts a python dictionary. No multipart encoder needed. – Mike B Feb 03 '21 at 18:32
  • I have followed this code, but my file is a `pdf`. Its not working. Can you help? – curiouscheese Jun 07 '23 at 06:01
  • in my case, I used requests first and coppy pasted the result from https://curlconverter.com/, include `data = '------WebKitFormBoundaryvFl3VlIejTySgos8\r\nContent-Disposition: form-data; name="file"; filename="785657825.png"\r\nContent-Type: image/png\r\n\r\n\r\n------WebKitFormBoundaryvFl3VlIejTySgos8--\r\n'` and then `requests.post('http://0.0.0.0:8000/removebackground',...,data=data)` but it did not work; thanks for `files={...}` – lam vu Nguyen Aug 18 '23 at 04:28
0

Below code is working for me:

** API Structure: **

File: api_routers.py

from fastapi import APIRouter, File, UploadFile, Query
router = APIRouter()
@router.post(path="{{API_PATH}}", tags=["Prediction"])
def prediction(id: str, uploadFile: UploadFile):
    ...
    {{CODE}}
    return response

Testing Code

File: test_api_router.py

import pytest
import os
from fastapi.testclient import TestClient
import.api_routers

client = TestClient(api_routers.router)

def test_prediction(constants):  
    # Use constants if fixture created
    file_path = "{{IMAGE PATH}}"
    if os.path.isfile(file_path):
        _files = {'uploadFile': open(file_path, 'rb')}
        response = client.post('{{API_PATH}}',
                           params={
                               "id": {{ID}}
                           },
                           files=_files
                           )
        assert response.status_code == 200
    else:
        pytest.fail("Scratch file does not exists.")