测试与FastAPI应用数据之间的差异

【squids.cn】 全网zui低价RDS,免费的迁移工具DBMotion、数据库备份工具DBTwin、SQL开发工具等

当使用两个不同的异步会话来测试FastAPI应用程序与数据库的连接时,可能会出现以下错误:

  1. 在测试中,在数据库中创建了一个对象(测试会话)。 

  2. 在应用程序中发出一个请求,在此请求中修改了这个对象(应用会话)。 

  3. 在测试中从数据库加载对象,但其中没有所需的更改(测试会话)。 

让我们找出发生了什么。

我们通常在应用程序和测试中使用两个不同的会话。

此外,在测试中,我们通常将会话包装在一个准备数据库进行测试的fixture中,测试完成后,所有内容都会被清理。

以下是应用程序的示例。

一个带有数据库连接的文件app/database.py:

""" Database settings file """
from typing import AsyncGenerator
​
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.orm import declarative_base
​
DATABASE_URL = "postgresql+asyncpg://user:password@host:5432/dbname"
engine = create_async_engine(DATABASE_URL, echo=True, future=True)
async_session = async_sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False)
​
​
async def get_session() -> AsyncGenerator:""" Returns async session """async with async_session() as session:yield session
​
Base = declarative_base()

一个带有模型描述的文件app/models.py: 

""" Model file """from sqlalchemy import Integer, Stringfrom sqlalchemy.orm import Mapped, mapped_columnfrom .database import Baseclass Lamp(Base):   """ Lamp model """   __tablename__ = 'lamps'   id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)   status: Mapped[str] = mapped_column(String, default="off")

一个带有端点描述的文件app/main.py: 

""" Main file """import loggingfrom fastapi import FastAPI, Dependsfrom sqlalchemy import selectfrom sqlalchemy.ext.asyncio import AsyncSessionfrom .database import get_sessionfrom .models import Lampapp = FastAPI()@app.post("/lamps/{lamp_id}/on")async def check_lamp(       lamp_id: int,       session: AsyncSession = Depends(get_session)) -> dict:   """ Lamp on endpoint """   results = await session.execute(select(Lamp).where(Lamp.id == lamp_id))   lamp = results.scalar_one_or_none()   if lamp:       logging.error("Status before update: %s", lamp.status)       lamp.status = "on"       session.add(lamp)       await session.commit()       await session.refresh(lamp)       logging.error("Status after update: %s", lamp.status)   return {}

我特意在示例中添加了日志记录和一些其他请求,以使其更加清晰。

这里,使用Depends创建了一个会话。

以下是带有测试示例的文件tests/test_lamp.py:

""" Test lamp """
import logging
from typing import AsyncGenerator
​
import pytest
import pytest_asyncio
from httpx import AsyncClient
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
​
from app.database import Base, engine
from app.main import app, Lamp
​
​
@pytest_asyncio.fixture(scope="function", name="test_session")
async def test_session_fixture() -> AsyncGenerator:""" Async session fixture """async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
​async with async_session() as session:async with engine.begin() as conn:await conn.run_sync(Base.metadata.create_all)
​yield session
​async with engine.begin() as conn:await conn.run_sync(Base.metadata.drop_all)
​await engine.dispose()
​
​
@pytest.mark.asyncio
async def test_lamp_on(test_session):""" Test lamp switch on """lamp = Lamp()test_session.add(lamp)await test_session.commit()await test_session.refresh(lamp)
​logging.error("New client status: %s", lamp.status)assert lamp.status == "off"
​async with AsyncClient(app=app, base_url="http://testserver") as async_client:response = await async_client.post(f"/lamps/{lamp.id}/on")assert response.status_code == 200results = await test_session.execute(select(Lamp).where(Lamp.id == lamp.id))new_lamp = results.scalar_one_or_none()logging.error("Updated status: %s", new_lamp.status)
​assert new_lamp.status == "on"

这是一个常规的Pytest,它在一个fixture中获取到数据库的会话。在返回会话之前,此fixture会先创建所有的表格,使用之后,它们会被删除。

请再次注意,在测试中,我们使用来自test_session fixture的会话,而在主代码中,我们使用来自app/database.py文件的会话。尽管我们使用相同的引擎,但是生成的会话是不同的。这一点很重要。

预期的数据库请求序列

应从数据库返回status = on。

在测试中,我首先在数据库中创建一个对象。这是通过来自测试的会话进行的常规INSERT。我们称其为Session 1。此时,只有这个会话连接到了数据库。应用程序会话尚未连接。

在创建对象之后,我执行了一个刷新操作。这是通过Session 1对新创建的对象进行的SELECT,并通过实例更新。

结果,我确保对象正确创建,并且status字段填充了所需的值 - off。

然后,我对/lamps/1/on端点执行一个POST请求。这是打开灯的操作。为了使示例更短,我没有使用fixture。一旦请求开始工作,将创建一个新的数据库会话。我们称其为Session 2。使用这个会话,我从数据库加载所需的对象。我将状态输出到日志。它是off。之后,我更新了这个状态并在数据库中保存了更新。数据库收到了一个请求:

BEGIN (implicit)UPDATE lamps SET status=$1::VARCHAR WHERE lamps.id = $2::INTEGERparameters: ('on', 1)COMMIT

请注意,也存在COMMIT命令。尽管事务是隐式的,但其结果在其他会话中在COMMIT之后立即可用。

接下来,我使用refresh发出请求,从数据库获取更新后的对象。我输出状态。现在它的值是on。

看起来一切都应该正常工作。端点停止工作,关闭Session 2,并将控制权转移到测试。

在测试中,我从Session 1发出常规请求,以获取修改过的对象。但在状态字段中,我看到的值是off。

以下是代码中操作序列的方案。

代码中的操作序列

代码中的操作序列 同时,根据所有日志,最后一个SELECT请求被执行并返回了status = on。此刻,数据库中的其值肯定等于on。这是engine asyncpg响应SELECT请求时接收的值。

那么,发生了什么?

发生了以下情况。

结果是,为获取新对象而做的请求并没有更新当前的对象,而是找到并使用了现有的对象。一开始,我使用ORM添加了一个灯泡对象。我在另一个会话中更改了它。当更改完成时,当前会话对此更改一无所知。并且在Session 2中进行的提交并未在Session 1中请求expire_all方法。

为了修复这个问题,你可以做以下操作之一:

  1. 对于测试和应用程序使用共享会话。

  2. 刷新实例,而不是尝试从数据库中获取它。

  3. 强制使实例过期。

  4. 关闭会话。

依赖性覆盖

为了使用相同的会话,您可以简单地使用我在测试中创建的那个覆盖应用程序中的会话。这很简单。

为此,我们需要在测试中添加以下代码:

async def _override_get_db():   yield test_sessionapp.dependency_overrides[get_session] = _override_get_db

如果你愿意,可以将这部分包装成一个夹具,以便在所有测试中使用。

所得到的算法将如下所示:

代码中使用依赖性覆盖时的步骤

下面是带有会话替代的测试代码:

@pytest.mark.asyncioasync def test_lamp_on(test_session):   """ Test lamp switch on """   async def _override_get_db():       yield test_session   app.dependency_overrides[get_session] = _override_get_db   lamp = Lamp()   test_session.add(lamp)   await test_session.commit()   await test_session.refresh(lamp)   logging.error("New client status: %s", lamp.status)   assert lamp.status == "off"   async with AsyncClient(app=app, base_url="http://testserver") as async_client:       response = await async_client.post(f"/lamps/{lamp.id}/on")       assert response.status_code == 200   results = await test_session.execute(select(Lamp).where(Lamp.id == 1))   new_lamp = results.scalar_one_or_none()   logging.error("Updated status: %s", new_lamp.status)   assert new_lamp.status == "on"

但是,如果应用程序使用多个会话(这是可能的),那么这可能不是最佳方法。此外,如果在被测试的函数中没有调用commit或rollback,那么这也将无济于事。

刷新 

第二种解决方案是最简单和最有逻辑的。我们不应该创建一个新的请求来获取一个对象。为了更新,处理端点请求后立即调用刷新就足够了。在内部,它调用expires,这导致保存的实例不用于新的请求,并且数据重新填充。这种解决方案是最有逻辑的,也最容易理解。

await test_session.refresh(lamp)

之后,你不需要再试图加载new_lamp对象,只需检查相同的lamp。

以下是使用刷新的代码方案。

使用刷新时的代码中的步骤 

以下是带有更新的测试代码。

@pytest.mark.asyncioasync def test_lamp_on(test_session):   """ Test lamp switch on """   lamp = Lamp()   test_session.add(lamp)   await test_session.commit()   await test_session.refresh(lamp)   logging.error("New client status: %s", lamp.status)   assert lamp.status == "off"   async with AsyncClient(app=app, base_url="http://testserver") as async_client:       response = await async_client.post(f"/lamps/{lamp.id}/on")       assert response.status_code == 200   await test_session.refresh(lamp)   logging.error("Updated status: %s", lamp.status)   assert lamp.status == "on"

过期 

但是,如果我们更改了很多对象,最好调用expire_all。然后,所有实例都将从数据库中读取,一致性不会被破坏。

test_session.expire_all()

你还可以在特定实例甚至实例属性上调用expire。

test_session.expire(lamp)

在这些调用之后,你将不得不手动从数据库中读取对象。

以下是使用过期时代码中的步骤序列。

使用过期时的代码中的步骤

使用过期时的代码中的步骤 以下是带有过期的测试代码。

@pytest.mark.asyncioasync def test_lamp_on(test_session):   """ Test lamp switch on """   lamp = Lamp()   test_session.add(lamp)   await test_session.commit()   await test_session.refresh(lamp)   logging.error("New client status: %s", lamp.status)   assert lamp.status == "off"   async with AsyncClient(app=app, base_url="http://testserver") as async_client:       response = await async_client.post(f"/lamps/{lamp.id}/on")       assert response.status_code == 200   test_session.expire_all()   # OR:   # test_session.expire(lamp)   results = await test_session.execute(select(Lamp).where(Lamp.id == 1))   new_lamp = results.scalar_one_or_none()   logging.error("Updated status: %s", new_lamp.status)   assert new_lamp.status == "on"

关闭 

实际上,使用会话终止的最后一种方法也调用了expire_all,但会话可以进一步使用。当读取新数据时,我们将获得最新的对象。

await test_session.close()

这应该在应用程序请求完成之后并在检查开始之前立即调用。

以下是使用关闭时代码中的步骤。

使用关闭时的代码中的步骤

以下是带有会话关闭的测试代码。

@pytest.mark.asyncioasync def test_lamp_on(test_session):   """ Test lamp switch on """   lamp = Lamp()   test_session.add(lamp)   await test_session.commit()   await test_session.refresh(lamp)   logging.error("New client status: %s", lamp.status)   assert lamp.status == "off"   async with AsyncClient(app=app, base_url="http://testserver") as async_client:       response = await async_client.post(f"/lamps/{lamp.id}/on")       assert response.status_code == 200   await test_session.close()   results = await test_session.execute(select(Lamp).where(Lamp.id == 1))   new_lamp = results.scalar_one_or_none()   logging.error("Updated status: %s", new_lamp.status)   assert new_lamp.status == "on"

调用 rollback() 也会有帮助。它也调用 expire_all,但它明确地回滚了事务。如果需要执行事务,commit() 也会执行 expire_all。但在这个例子中,既不需要回滚也不需要提交,因为测试中的事务已经完成,应用程序中的事务不会影响来自测试的会话。

实际上,此功能仅在SQLAlchemy ORM的异步模式中在事务中工作。然而,在代码中我确实向数据库发出请求以获取新对象的行为看起来似乎不合逻辑,如果它仍然返回一个缓存的对象,而不是从数据库强制接收的对象。当调试代码时,这有点令人困惑。但当正确使用时,这就是它应有的样子。

结论 

在异步模式下使用SQLAlchemy ORM,您必须并行跟踪事务和线程中的会话。如果所有这些看起来太复杂,那么使用SQLAlchemy ORM的同步模式。它里面的一切都简单得多。

作者:Aleksei Sharypov

更多内容请关注公号【云原生数据库】

squids.cn,云数据库RDS,迁移工具DBMotion,云备份DBTwin等数据库生态工具。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/82886.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

QT记事本+登陆界面的简单实现

主体头文件 #ifndef JSB_H #define JSB_H#include <QMainWindow> #include <QMenuBar>//菜单栏 #include <QToolBar>//工具栏 #include <QStatusBar>//状态栏 #include <QTextEdit>//文本 #include <QLabel>//标签 #include <QDebug&g…

Golang gorm 一对一关系

一对一关系 一对一关系比较少&#xff0c;一般用于表的扩展例如一张用户表&#xff0c;有很多字段那么就可以把它拆分为两张表&#xff0c;常用的字段放主表&#xff0c;不常用的字段放详情表。 针对用户表来说可以通过user去点出userinfo。 创建表和插入数据 package mainimp…

纯js实现html指定页面导出word

因为最近做了范文网站需要&#xff0c;所以要下载为word文档&#xff0c;如果php进行处理&#xff0c;很吃后台服务器&#xff0c;所以想用前端进行实现。查询github发现&#xff0c;确实有这方面的插件。 js导出word文档所需要的两个插件&#xff1a; FileSaver.js jquery.w…

【AI视野·今日NLP 自然语言处理论文速览 第三十六期】Tue, 19 Sep 2023

AI视野今日CS.NLP 自然语言处理论文速览 Tue, 19 Sep 2023 (showing first 100 of 106 entries) Totally 106 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Computation and Language Papers Speaker attribution in German parliamentary debates with QLoRA-ada…

如何理解高效IO

目录 前言 1.如何理解高效的IO 2.五种IO模型 3.非阻塞IO 4.非阻塞代码编写 总结 前言 哈喽&#xff0c;很高兴和大家见面&#xff01;今天我们要介绍的关于IO的话题&#xff0c;在计算机中IO是非常常规的操作&#xff0c;例如将数据显示到外设&#xff0c;或者将数据从主…

【LeetCode75】第五十九题 第N个泰波那契数

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 题目顾名思义&#xff0c;让我们求出第N个泰波那契数&#xff0c;也就是除了开头三个数之外&#xff0c;第四个数开始就是等于前三个数之…

基于 Alpine 环境构建 aspnetcore6-runtime 的 Docker 镜像

关于 Alpine Linux 此处就不再过多讲述&#xff0c;请自行查看相关文档。 .NET 支持的体系结构 下表列出了当前支持的 .NET 体系结构以及支持它们的 Alpine 版本。 这些版本在 .NET 到达支持终止日期或 Alpine 的体系结构受支持之前仍受支持。请注意&#xff0c;Microsoft 仅正…

Jenkins+Gitee+Docker+Ruoyi项目前后端分离部署

前言 描述&#xff1a;本文主要是用来记录 如何用标题上的技术&#xff0c;部署到云服务器上通过ip正常访问。 一、总览 1.1、Docker做的事 拉取 mysql 镜像拉取 redis 镜像拉取 jdk 镜像拉取 nginx 镜像 解释说明&#xff1a;前端项目的打包文件放在 nginx容器运行。后端…

PWMADC重要参数

频率的计算 1、 ARR&#xff08;TIM_Period&#xff09; 是计数值&#xff1b; 2、 PSC&#xff08;TIM_Prescaler&#xff09; 是预分频值。 频率计算公式&#xff1a;Fpwm 主频 / ((ARR1)*(PSC1))(单位&#xff1a;Hz) 占空比的计算 计算公式&#xff1a;duty circle TIM3…

部署大数据平台详细教程以及遇到的问题解答(ubuntu18.04下安装ambari2.7.3+HDP3.1.0)

节点准备: 我搭建的是3台,节点可以随意。建议最少是3台 hostname ip 角色 ubuntu-1804-1 172.21.73.53 从节点 ubuntu-1804-2 172.21.73.54 主节点 ubuntu-1804-3 172.21.73.55 从节点 一:关闭所有节点的防火墙 sudo ufw disable二:配置时钟同步NTP 所有节点安装ntp sud…

Lua学习笔记:探究package

前言 本篇在讲什么 理解Lua的package 本篇需要什么 对Lua语法有简单认知 对C语法有简单认知 依赖Visual Studio工具 本篇的特色 具有全流程的图文教学 重实践&#xff0c;轻理论&#xff0c;快速上手 提供全流程的源码内容 ★提高阅读体验★ &#x1f449; ♠ 一级…

腾讯云微服务平台 TSF 异地多活单元化能力重磅升级

导语 2023腾讯全球数字生态大会已于9月7-8日完美落幕&#xff0c;40专场活动展示了腾讯最新的前沿技术、核心产品、解决方案。 微服务与消息队列专场&#xff0c;腾讯云微服务平台 TSF 产品经理张桢带来了《腾讯云微服务平台 TSF 异地多活单元化能力重磅升级》的精彩演讲。本…

Unity的配置文件在安卓路径下使用的方法

Unity的配置文件在安卓路径下使用的方法 前言 之前我做过的很多使用配置文件的Unity项目&#xff0c;后面的有些项目也有在安卓路径下读取json文件的需求。这几天有个需求是获取在安卓路径下配置文件里的数据&#xff0c;我在网上查了一些案例&#xff0c;简单实现了这个需求…

内网穿透的应用-Cloudreve搭建云盘系统,并实现随时访问

文章目录 1、前言2、本地网站搭建2.1 环境使用2.2 支持组件选择2.3 网页安装2.4 测试和使用2.5 问题解决 3、本地网页发布3.1 cpolar云端设置3.2 cpolar本地设置 4、公网访问测试5、结语 1、前言 自云存储概念兴起已经有段时间了&#xff0c;各互联网大厂也纷纷加入战局&#…

小样本目标检测:ECEA: Extensible Co-Existing Attention for Few-Shot Object Detection

论文作者&#xff1a;Zhimeng Xin,Tianxu Wu,Shiming Chen,Yixiong Zou,Ling Shao,Xinge You 作者单位&#xff1a;Huazhong University of Science and Technology; UCAS-Terminus AI Lab 论文链接&#xff1a;http://arxiv.org/abs/2309.08196v1 内容简介&#xff1a; 1&…

C++const关键字

本文旨在讲解C中相关const关键字的详解&#xff0c;希望读完本篇文章&#xff0c;可以让诸位对C中的const关键字有更深一步的认识&#xff01; 在C中&#xff0c;若想让类中某一个变量不再改变&#xff0c;可以使用const关键字进行修饰&#xff0c;让数据不被修改&#xff0c;使…

2023-09-19 LeetCode每日一题(打家劫舍 IV)

2023-09-19每日一题 一、题目编号 2560. 打家劫舍 IV二、题目链接 点击跳转到题目位置 三、题目描述 沿街有一排连续的房屋。每间房屋内都藏有一定的现金。现在有一位小偷计划从这些房屋中窃取现金。 由于相邻的房屋装有相互连通的防盗系统&#xff0c;所以小偷 不会窃取…

xp 系统 安装 python 2.7 ide pip

1 下载python http://www.python.org/ftp/python/ python-2.7.2.msi 安装完需要设置环境变量 2 下载 setuptools setuptools-0.6c11.win32-py2.7.exe https://pypi.tuna.tsinghua.edu.cn/simple/setuptools/ 3 下载 pip &#xff0c;python 2.7 最高支持 pip 20.3.4 https:…

Palantir的“英伟达时刻”即将到来

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 总结 &#xff08;1&#xff09;由于投资者对生成式人工智能的兴趣持续增加&#xff0c;Palantir的股价一直在上涨。 &#xff08;2&#xff09;Palantir已经连续三个季度实现了GAAP盈利&#xff0c;并将很快有资格被纳入标…

027-从零搭建微服务-搜索服务(一)

写在最前 如果这个项目让你有所收获&#xff0c;记得 Star 关注哦&#xff0c;这对我是非常不错的鼓励与支持。 源码地址&#xff08;后端&#xff09;&#xff1a;https://gitee.com/csps/mingyue 源码地址&#xff08;前端&#xff09;&#xff1a;https://gitee.com/csps…