让我们先解释一下你做错了什么。
FastAPI's TestClient 只是Starlette's TestClient 的再导出,requests.Session 的子类。 requests 库的 post 方法具有以下签名:
def post(self, url, data=None, json=None, **kwargs):
r"""Sends a POST request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
object to send in the body of the :class:`Request`.
:param json: (optional) json to send in the body of the :class:`Request`.
:param \*\*kwargs: Optional arguments that ``request`` takes.
:rtype: requests.Response
"""
你的代码
response = client.post(
"/items/",
{"id": "baz", "title": "A test title", "description": "A test description"},
"Baz",
)
做错了几件事:
- 它将参数传递给 both
data 和 json 参数,这是错误的,因为您不能有 2 个不同的请求主体。您要么传入data 或 json,但不能同时传入。 data 通常用于来自 HTML 表单的表单编码输入,而 json 用于原始 JSON 对象。请参阅requests docs on "More complicated POST requests"。
- 请求库将简单地删除
json 参数,因为:
注意,如果data 或files 被传递,json 参数将被忽略。
- 它将纯字符串
"Baz" 传递给json 参数,这不是有效的JSON 对象。
-
data 参数需要表单编码的数据。
FastAPI 在响应中返回的完整错误是:
def test_create_item():
response = client.post(
"/items/", {"id": "baz", "title": "A test title", "description": "A test description"}, "Baz"
)
print(response.status_code)
print(response.reason)
return response.json()
# 422 Unprocessable Entity
# {'detail': [{'loc': ['query', 'key'],
# 'msg': 'field required',
# 'type': 'value_error.missing'},
# {'loc': ['body'],
# 'msg': 'value is not a valid dict',
# 'type': 'type_error.dict'}]}
第一个错误表示查询中缺少 key,这意味着路由参数 key 值 "Baz" 不在请求正文中,FastAPI 尝试从查询参数中查找它(请参阅 FastAPI 文档Request body + path + query parameters)。
第二个错误来自我上面列出的关于 data 未正确形式编码的第 4 点(当您将 dict 值包装在 json.dumps 中时,该错误确实消失了,但这并不重要,也不是它的一部分解决方案)。
你说in a comment 你正试图做与FastAPI Testing Tutorial 相同的事情。该教程与您的代码的不同之处在于将 Item 对象的所有属性发布在 1 个正文中,并且它使用了 .post 的 json= 参数。
现在是解决方案!
解决方案 #1:有一个单独的类用于使用键发布项目属性
在这里,您需要 2 个类,一个具有 key 属性,用于 POST 请求正文(我们称之为 NewItem),而您当前的一个 Item 用于内部数据库和响应模型。然后,您的路由函数将只有 1 个参数 (new_item),您可以从该对象中获取 key。
main.py
class Item(BaseModel):
id: str
title: str
description: Optional[str] = None
class NewItem(Item):
key: str
@app.post("/items/", response_model=Item)
async def create_item(new_item: NewItem):
# See Pydantic https://pydantic-docs.helpmanual.io/usage/exporting_models/#modeldict
# Also, Pydantic by default will ignore the extra attribute `key` when creating `Item`
item = Item(**new_item.dict())
print(item)
fake_db[new_item.key] = item
return item
对于测试.post 代码,使用json= 传递1 个字典中的所有字段。
test_main.py
def test_create_item():
response = client.post(
"/items/",
json={
"key": "Baz",
"id": "baz",
"title": "A test title",
"description": "A test description",
},
)
print(response.status_code, response.reason)
return response.json()
输出
id='baz' title='A test title' description='A test description'
200 OK
{'description': 'A test description', 'id': 'baz', 'title': 'A test title'}
解决方案 #2:有 2 个身体部位,1 个用于项目属性,1 个用于键
您可以改为这样构建 POSTed 正文:
{
"item": {
"id": "baz",
"title": "A test title",
"description": "A test description",
},
"key": "Baz",
},
您在嵌套字典中拥有Item 属性,然后在与item 相同的级别中拥有一个简单的key-值对。 FastAPI 可以处理这个问题,请参阅Singular values in body 上的文档,它非常适合您的示例:
例如,扩展之前的模型,您可以决定除了item 和user 之外,还希望在同一主体中拥有另一个密钥importance。
如果你按原样声明它,因为它是一个奇异值,FastAPI 会假定它是一个query 参数。
但您可以指示 FastAPI 将其视为另一个主体 key 使用 Body
注意我强调的部分,关于告诉 FastAPI 在同一个正文中查找key。这里重要的是参数名称item 和key 与请求正文中的名称匹配。
main.py
from fastapi import Body, FastAPI
class Item(BaseModel):
id: str
title: str
description: Optional[str] = None
@app.post("/items/", response_model=Item)
async def create_item(item: Item, key: str = Body(...)):
print(item)
fake_db[key] = item
return item
同样,为了发出.post 请求,请使用json= 传递整个字典。
test_main.py
def test_create_item():
response = client.post(
"/items/",
json={
"item": {
"id": "baz",
"title": "A test title",
"description": "A test description",
},
"key": "Baz",
},
)
print(response.status_code, response.reason)
return response.json()
输出
id='baz' title='A test title' description='A test description'
200 OK
{'description': 'A test description', 'id': 'baz', 'title': 'A test title'}