如何在FastAPI中为Pydantic模型编写测试?

13

我刚刚开始使用FastAPI,但是我不知道如何编写一个使用pytest测试Pydantic模型的单元测试。

以下是一个示例Pydantic模型:

class PhoneNumber(BaseModel):
    id: int
    country: str
    country_code: str
    number: str
    extension: str

我想通过创建一个示例PhoneNumber实例来测试这个模型,并确保PhoneNumber实例与字段类型相符。例如:
PhoneNumber(1, "country", "code", "number", "extension")

接下来,我想断言PhoneNumber.country等于"country"。


你期望测试检查什么?它与FastAPI有什么关系?因为你可以使用pydantic模型而不必使用FastAPI。 - Gino Mempin
2
@Mark 是的,没错。我已经做了更改。感谢你指出来。 - PercySherlock
7
一般来说,你不应该写这样的测试。Pydantic有一个很好的测试套件(包括像你建议的 单元测试)。你的测试应该覆盖你编写的代码和逻辑,而不是导入的软件包。 - Mark
3
由于您标记了 FastAPI,如果您将此模型用作路由的一部分(无论是作为请求参数还是响应模型),那么更有用的测试是检查调用您的路由/API是否正确使用您的模型(例如,传递一个 JSON 主体是否正确地转换为您的 PhoneNumber 模型)。 - Gino Mempin
2
@GinoMempin 我现在明白了。谢谢! - PercySherlock
显示剩余2条评论
1个回答

28

你想要的测试可以很容易地使用pytest进行:

import pytest

def test_phonenumber():
    pn = PhoneNumber(id=1, country="country", country_code="code", number="number", extension="extension")

    assert pn.id == 1
    assert pn.country == 'country'
    assert pn.country_code == 'code'
    assert pn.number == 'number'
    assert pn.extension == 'extension'

但我同意这个评论

总的来说,你不应该像这样编写测试。 Pydantic已经有很好的测试套件(包括像您提议的单元测试)。你的测试应该覆盖你编写的代码和逻辑,而不是导入的包。

如果你有一个像PhoneNumber这样没有任何特殊/复杂验证的模型,那么写一个仅实例化它并检查属性的测试就不会那么有用。类似于这样的测试就像在测试 Pydantic 本身。

但是,如果你的模型有一些特殊/复杂的验证函数,例如它检查countrycountry_code是否匹配:

from pydantic import BaseModel, root_validator

class PhoneNumber(BaseModel):
    ...

    @root_validator(pre=True)
    def check_country(cls, values):
        """Check that country_code is the 1st 2 letters of country"""
        country: str = values.get('country')
        country_code: str = values.get('country_code')
        if not country.lower().startswith(country_code.lower()):
            raise ValueError('country_code and country do not match')
        return values

那么针对该特定行为的单元测试将更加有用:

import pytest

def test_phonenumber_country_code():
    """Expect test to fail because country_code and country do not match"""
    with pytest.raises(ValueError):
        PhoneNumber(id=1, country='JAPAN', country_code='XY', number='123', extension='456')

另外,正如我在评论中提到的,如果您提到了FastAPI,并且您将此模型用作路由定义的一部分(无论是请求参数还是响应模型),那么更有用的测试将是确保您的路由能够正确使用您的模型。

@app.post("/phonenumber")
async def add_phonenumber(phonenumber: PhoneNumber):
    """The model is used here as part of the Request Body"""
    # Do something with phonenumber
    return JSONResponse({'message': 'OK'}, status_code=200)
from fastapi.testclient import TestClient

client = TestClient(app)

def test_add_phonenumber_ok():
    """Valid PhoneNumber, should be 200/OK"""
    # This would be what the JSON body of the request would look like
    body = {
        "id": 1,
        "country": "Japan",
        "country_code": "JA",
        "number": "123",
        "extension": "81",
    }
    response = client.post("/phonenumber", json=body)
    assert response.status_code == 200


def test_add_phonenumber_error():
    """Invalid PhoneNumber, should be a validation error"""
    # This would be what the JSON body of the request would look like
    body = {
        "id": 1,
        "country": "Japan",
                             # `country_code` is missing
        "number": 99999,     # `number` is int, not str
        "extension": "81",
    }
    response = client.post("/phonenumber", json=body)
    assert response.status_code == 422
    assert response.json() == {
        'detail': [{
            'loc': ['body', 'country_code'],
            'msg': 'field required',
            'type': 'value_error.missing'
        }]
    }

2
感谢您提供这么详细的答案,我确实学到了很多。 - PercySherlock

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接