FastAPI 的单元测试

  • 对于服务端来说,通常会对功能进行单元测试,也称白盒测试
  • FastAPI 集成了第三方库,让我们可以快捷的编写单元测试
  • FastAPI 的单元测试是基于 Pytest + Request 的 

 

Pytest 学习​

 

TestClient 简单的栗子



#!usr/bin/env python
# -*- coding:utf-8 _*-
"""
# author: 小菠萝测试笔记

# time: 2021/9/29 10:55 下午
# file: 37_pytest.py
"""
import uvicorn
from fastapi import FastAPI
from fastapi.testclient import TestClient

app = FastAPI()


@app.get("/")
async def read_main():
return {"msg": "Hello World"}


# 声明一个 TestClient,把 FastAPI() 实例对象传进去
client = TestClient(app)


# 测试用
def test_read_main():
# 请求 127.0.0.1:8080/
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"msg": "Hello World"}


if __name__ == '__main__':
uvicorn.run(app="37_pytest:app", reload=True, host="127.0.0.1", port=8080)


  

在该文件夹下的命令行敲



pytest 37_pytest.py


 

运行结果

FastAPI(43)- 基于 pytest + requests 进行单元测试_单元测试

 

TestClient 的源码解析

继承了 requests 库的 Session

FastAPI(43)- 基于 pytest + requests 进行单元测试_单元测试_02

所以可以像使用 requests 库一样使用 TestClient,拥有 requests 所有方法、属性

 

重写了 Session.requests 方法

FastAPI(43)- 基于 pytest + requests 进行单元测试_json_03

重写了 requests 方法,不过只是加了一句 url = urljoin(self.base_url, url) url 拼接代码,还有给函数参数都加了类型指示,更加完善啦~

 

自定义 websocket 连接方法

FastAPI(43)- 基于 pytest + requests 进行单元测试_数据_04

后面学到 webSocket 再详细讲他

 

重写了 __enter__、__exit__ 方法

FastAPI(43)- 基于 pytest + requests 进行单元测试_数据_05

  • Session 的这两个方法还是比较简陋的,TestClient 做了一次重写,主要是为了添加异步的功能(异步测试后面详解,这篇举栗子的都是普通函数 def)
  • 前面讲过有 __enter__、__exit__ 方法的对象都是上下文管理器,可以用 with .. as .. 语句来调用上下文管理器哦

 

.get() 方法

上面代码 client.get(),直接调用的就是 Session 提供的 get() 方法啦!

FastAPI(43)- 基于 pytest + requests 进行单元测试_单元测试_06

 

复杂的测试场景

服务端



#!usr/bin/env python
# -*- coding:utf-8 _*-
"""
# author: 小菠萝测试笔记

# time: 2021/9/29 10:55 下午
# file: s37_pytest.py
"""
import uvicorn
from fastapi import FastAPI
from fastapi.testclient import TestClient

app = FastAPI()


@app.get("/")
async def read_main():
return {"msg": "Hello World"}


# 声明一个 TestClient,把 FastAPI() 实例对象传进去
client = TestClient(app)


# 测试用
def test_read_main():
# 请求 127.0.0.1:8080/
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"msg": "Hello World"}


from typing import Optional

from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel

# 模拟真实 token
fake_secret_token = "coneofsilence"

# 模拟真实数据库
fake_db = {
"foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"},
"bar": {"id": "bar", "title": "Bar", "description": "The bartenders"},
}

app = FastAPI()


class Item(BaseModel):
id: str
title: str
description: Optional[str] = None


# 接口一:查询数据
@app.get("/items/{item_id}", response_model=Item)
async def read_main(item_id: str, x_token: str = Header(...)):
# 1、校验 token 失败
if x_token != fake_secret_token:
raise HTTPException(status_code=400, detail="x-token 错误")

# 2、若数据库没有对应数据
if item_id not in fake_db:
raise HTTPException(status_code=404, detail="找不到 item_id")
# 3、找到数据则返回
return fake_db[item_id]


# 接口二:创建数据
@app.post("/items/", response_model=Item)
async def create_item(item: Item, x_token: str = Header(...)):
# 1、校验 token 失败
if x_token != fake_secret_token:
raise HTTPException(status_code=400, detail="x-token 错误")

# 2、若数据库已经存在相同 id 的数据
if item.id in fake_db:
raise HTTPException(status_code=400, detail="找不到 item_id")

# 3、添加数据到数据库
fake_db[item.id] = item

# 4、返回添加的数据
return item


if __name__ == '__main__':
uvicorn.run(app="s37_test_pytest:app", reload=True, host="127.0.0.1", port=8080)


 

单元测试 



#!usr/bin/env python
# -*- coding:utf-8 _*-
"""
# author: 小菠萝测试笔记

# time: 2021/9/29 10:55 下午
# file: s37_pytest.py
"""
from fastapi.testclient import TestClient
from .s37_test_pytest import app

client = TestClient(app)


def test_read_item():
expect = {"id": "foo", "title": "Foo", "description": "There goes my hero"}
headers = {"x-token": "coneofsilence"}
resp = client.get("/items/foo", headers=headers)
assert resp.status_code == 200
assert resp.json() == expect


def test_read_item_error_header():
expect = {"detail": "x-token 错误"}
headers = {"x-token": "test"}
resp = client.get("/items/foo", headers=headers)
assert resp.status_code == 400
assert resp.json() == expect


def test_read_item_error_id():
expect = {"detail": "找不到 item_id"}
headers = {"x-token": "coneofsilence"}
resp = client.get("/items/foos", headers=headers)
assert resp.status_code == 404
assert resp.json() == expect


def test_create_item():
body = {"id": "foos", "title": "Foo", "description": "There goes my hero"}
headers = {"x-token": "coneofsilence"}
resp = client.post("/items/", json=body, headers=headers)
assert resp.status_code == 200
assert resp.json() == body


def test_create_item_error_header():
body = {"id": "foo", "title": "Foo", "description": "There goes my hero"}
expect = {"detail": "x-token 错误"}
headers = {"x-token": "test"}
resp = client.post("/items/", json=body, headers=headers)
assert resp.status_code == 400
assert resp.json() == expect


def test_create_item_error_id():
expect = {"detail": "找不到 item_id"}
body = {"id": "foo", "title": "Foo", "description": "There goes my hero"}
headers = {"x-token": "coneofsilence"}
resp = client.post("/items/", json=body, headers=headers)
assert resp.status_code == 400
assert resp.json() == expect


 

命令行运行



pytest test.py -sq


  

运行结果



> pytest s37_pytest.py -sq
......
6 passed in 0.40s