测试与FastAPI应用数据之间的差异
【squids.cn】 全网zui低价RDS,免费的迁移工具DBMotion、数据库备份工具DBTwin、SQL开发工具等
当使用两个不同的异步会话来测试FastAPI应用程序与数据库的连接时,可能会出现以下错误:
-
在测试中,在数据库中创建了一个对象(测试会话)。
-
在应用程序中发出一个请求,在此请求中修改了这个对象(应用会话)。
-
在测试中从数据库加载对象,但其中没有所需的更改(测试会话)。
让我们找出发生了什么。
我们通常在应用程序和测试中使用两个不同的会话。
此外,在测试中,我们通常将会话包装在一个准备数据库进行测试的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方法。
为了修复这个问题,你可以做以下操作之一:
-
对于测试和应用程序使用共享会话。
-
刷新实例,而不是尝试从数据库中获取它。
-
强制使实例过期。
-
关闭会话。
依赖性覆盖
为了使用相同的会话,您可以简单地使用我在测试中创建的那个覆盖应用程序中的会话。这很简单。
为此,我们需要在测试中添加以下代码:
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_sessionapp.dependency_overrides[get_session] = _override_get_dblamp = 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 == 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 == 200await 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 == 200test_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 == 200await 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等数据库生态工具。
相关文章:
测试与FastAPI应用数据之间的差异
【squids.cn】 全网zui低价RDS,免费的迁移工具DBMotion、数据库备份工具DBTwin、SQL开发工具等 当使用两个不同的异步会话来测试FastAPI应用程序与数据库的连接时,可能会出现以下错误: 在测试中,在数据库中创建了一个对象&#x…...
WebStorm 2023年下载、安装教程、亲测有效
文章目录 简介安装步骤常用快捷键 简介 WebStorm 是JetBrains公司旗下一款JavaScript 开发工具。已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的…...
k8s储存卷
卷的类型 In-Tree存储卷插件 ◼ 临时存储卷 ◆emptyDir ◼ 节点本地存储卷 ◆hostPath, local ◼ 网络存储卷 ◆文件系统:NFS、GlusterFS、CephFS和Cinder ◆块设备:iSCSI、FC、RBD和vSphereVolume ◆存储平台:Quobyte、PortworxVolume、Sto…...
【解决Win】“ 无法打开某exe提示无法成功完成操作,因为文件包含病毒或潜在的垃圾软件“
在下载某个应用程序,打开时出现了“无法成功完成操作因为文件包含病毒或潜在垃圾”的提示,遇到这个情况怎么解决? 下面为大家分享故障原因及具体的处理方法。 故障原因 是由于杀毒 防护等原因引起的。 解决方案 打开Windows 安全中心 选择…...
SpringBoot调用ChatGPT-API实现智能对话
目录 一、说明 二、代码 2.1、对话测试 2.2、单次对话 2.3、连续对话 2.4、AI绘画 一、说明 我们在登录chatgpt官网进行对话是不收费的,但需要魔法。在调用官网的API时,在代码层面上使用,通过API KEY进行对话是收费的,不过刚…...
element-table出现错位解决方法
先看示例图,这个在开发中还是很常遇到的,在table切换不同数据时或者切换页面时,容易出现: 解决方法很简单,官方有提供方法: 我们可以在重新渲染数据后: this.$nextTick(() > {this.$refs.…...
DC电源模块具有不同的安装方式和安全规范
BOSHIDA DC电源模块具有不同的安装方式和安全规范 DC电源模块是将低压直流电转换为需要的输出电压的装置。它们广泛应用于各种领域和行业,如通信、医疗、工业、家用电器等。安装DC电源模块应严格按照相关的安全规范进行,以确保其正常运行和安全使用。 D…...
zabbix自定义监控、钉钉、邮箱报警
目录 一、实验准备 二、安装 三、添加监控对象 四、添加自定义监控项 五、监控mariadb 1、添加模版查看要求 2、安装mariadb、创建用户 3、创建用户文件 4、修改监控模版 5、在上述文件中配置路径 6、重启zabbix-agent验证 六、监控NGINX 1、安装NGINX,…...
短信、邮箱验证码本地可以,部署到服务器接口却不能使用
应对公司双验证要求,对本系统做邮箱、短信验证码登录,本地开发正常发送,到服务器上部署却使用失败,已全部解决,记录坑。 一、nginx拦截 先打开你的服务器 nginx.conf 看看有没有做接口拦截。(本地可能做Sp…...
Java web基础知识
Servlet Servlet是sun公司开发的动态web技术 sun在API中提供了一个接口叫做 Servlet ,一个简单的Servlet 程序只需要完成两个步骤 编写一个实现了Servlet接口的类 把这个Java部署到web服务器中 一般来说把实现了Servlet接口的java程序叫做,Servlet 初步…...
【Linux学习】01Linux初识与安装
Linux(B站黑马)学习笔记 01Linux初识与安装 文章目录 Linux(B站黑马)学习笔记前言01Linux初识与安装操作系统简述Linux初识虚拟机介绍安装VMware Workstation虚拟化软件VMware中安装CentOS7 Linux操作系统下载CentOS操作系统VMwa…...
android 将数据库中的 BLOB 对象动态加载为 XML,并设置到 Android Activity 的内容视图上
以下是一个示例代码,演示如何将数据库中的 BLOB 对象动态加载为 XML,并设置到 Android Activity 的内容视图上: ```java import android.app.Activity; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import and…...
Android12之强弱智能指针sp/wp循环引用死锁问题(一百六十六)
简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 人生格言: 人生从来没有捷径,只有行动才是治疗恐惧和懒惰的唯一良药. 更多原创,欢迎关注:Android…...
springboot自定义Json序列化返回,实现自动转换字典值
自定义序列化 原理 当你使用Spring Boot的Spring Data或者Spring MVC等组件来处理JSON序列化时,Spring框架会在需要将Java对象转换为JSON字符串时调用JsonSerializer。这通常发生在控制器方法返回JSON响应时,或者在将对象保存到数据库等操作中。 // 注册…...
Lostash同步Mysql数据到ElasticSearch(二)logstash脚本配置和常见坑点
1. logstash脚本编写(采用单文件对应单表实例) 新建脚本文件夹 cd /usr/local/logstash mkdir sql & cd sql vim 表名称.conf #如: znyw_data_gkb_logstash.conf 建立文件夹,保存资源文件更新Id mkdir -p /data/logstash/data/last_r…...
兔兔答题企业版1.0.0版本全网发布,同时开源前端页面到unicloud插件市场
项目说明 兔兔答题是用户端基于uniapp开发支持多端适配,管理端端采用TypeScriptVue3.jselement-plus,后端采用THinkPHP6PHP8Golang开发的一款在线答题应用程序。 问题反馈 线上预览地址 相关问题可以通过下方的二维码,联系到我。了解更多 …...
76、SpringBoot 整合 MyBatis------使用 sqlSession 作为 Dao 组件(就是ssm那一套,在 xml 写sql)
就是 ssm 那套,在xml 上面写sql ★ 基于SqlSession来实现DAO组件的方式 - MyBatis提供的Starter会自动在Spring容器中配置SqlSession(其实SqlSessionTemplate实现类)、并将它注入其他组件(如DAO组件)- DAO组件可直接…...
【ROS】RViz、Gazebo和Navigation的关系
1、RViz RViz(Robot Visualization,机器人可视化)是一个用于可视化机器人系统的开源工具,用于显示和调试机器人的传感器数据、状态信息和运动规划等。它是ROS(Robot Operating System)的一部分,是ROS中最常用的可视化工具之一。 RViz:“我们不生产数据只做数据的搬运…...
智能井盖:提升城市井盖安全管理效率
窨井盖作为城市基础设施的重要组成部分,其安全管理与城市的有序运行和群众的生产生活安全息息相关,体现城市管理和社会治理水平。当前,一些城市已经将智能化的窨井盖升级改造作为新城建的重要内容,推动窨井盖等“城市部件”配套建…...
JavaWeb开发-06-SpringBootWeb-MySQL
一.MySQL概述 1.安装、配置 官网下载地址:https://dev.mysql.com/downloads/mysql/ 2.数据模型 3.SQL简介 二.数据库设计-DDL 1.数据库 官网:http:// https://www.jetbrains.com/zh-cn/datagrip/ 2.表(创建、查询、修改、删除) #…...
uniapp 对接腾讯云IM群组成员管理(增删改查)
UniApp 实战:腾讯云IM群组成员管理(增删改查) 一、前言 在社交类App开发中,群组成员管理是核心功能之一。本文将基于UniApp框架,结合腾讯云IM SDK,详细讲解如何实现群组成员的增删改查全流程。 权限校验…...
STM32F4基本定时器使用和原理详解
STM32F4基本定时器使用和原理详解 前言如何确定定时器挂载在哪条时钟线上配置及使用方法参数配置PrescalerCounter ModeCounter Periodauto-reload preloadTrigger Event Selection 中断配置生成的代码及使用方法初始化代码基本定时器触发DCA或者ADC的代码讲解中断代码定时启动…...
Springcloud:Eureka 高可用集群搭建实战(服务注册与发现的底层原理与避坑指南)
引言:为什么 Eureka 依然是存量系统的核心? 尽管 Nacos 等新注册中心崛起,但金融、电力等保守行业仍有大量系统运行在 Eureka 上。理解其高可用设计与自我保护机制,是保障分布式系统稳定的必修课。本文将手把手带你搭建生产级 Eur…...
unix/linux,sudo,其发展历程详细时间线、由来、历史背景
sudo 的诞生和演化,本身就是一部 Unix/Linux 系统管理哲学变迁的微缩史。来,让我们拨开时间的迷雾,一同探寻 sudo 那波澜壮阔(也颇为实用主义)的发展历程。 历史背景:su的时代与困境 ( 20 世纪 70 年代 - 80 年代初) 在 sudo 出现之前,Unix 系统管理员和需要特权操作的…...
【Java_EE】Spring MVC
目录 Spring Web MVC 编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 编辑参数重命名 RequestParam 编辑编辑传递集合 RequestParam 传递JSON数据 编辑RequestBody …...
汇编常见指令
汇编常见指令 一、数据传送指令 指令功能示例说明MOV数据传送MOV EAX, 10将立即数 10 送入 EAXMOV [EBX], EAX将 EAX 值存入 EBX 指向的内存LEA加载有效地址LEA EAX, [EBX4]将 EBX4 的地址存入 EAX(不访问内存)XCHG交换数据XCHG EAX, EBX交换 EAX 和 EB…...
音视频——I2S 协议详解
I2S 协议详解 I2S (Inter-IC Sound) 协议是一种串行总线协议,专门用于在数字音频设备之间传输数字音频数据。它由飞利浦(Philips)公司开发,以其简单、高效和广泛的兼容性而闻名。 1. 信号线 I2S 协议通常使用三根或四根信号线&a…...
C/C++ 中附加包含目录、附加库目录与附加依赖项详解
在 C/C 编程的编译和链接过程中,附加包含目录、附加库目录和附加依赖项是三个至关重要的设置,它们相互配合,确保程序能够正确引用外部资源并顺利构建。虽然在学习过程中,这些概念容易让人混淆,但深入理解它们的作用和联…...
MyBatis中关于缓存的理解
MyBatis缓存 MyBatis系统当中默认定义两级缓存:一级缓存、二级缓存 默认情况下,只有一级缓存开启(sqlSession级别的缓存)二级缓存需要手动开启配置,需要局域namespace级别的缓存 一级缓存(本地缓存&#…...
9-Oracle 23 ai Vector Search 特性 知识准备
很多小伙伴是不是参加了 免费认证课程(限时至2025/5/15) Oracle AI Vector Search 1Z0-184-25考试,都顺利拿到certified了没。 各行各业的AI 大模型的到来,传统的数据库中的SQL还能不能打,结构化和非结构的话数据如何和…...
