如何配置 PostgreSQL 用于集成测试
在进行测试时,实现性能和可靠性至关重要。在本文中,我将解释如何为测试设置 PostgreSQL,并讨论一些常见的需要避免的陷阱。
隔离性是首要目标
在我们深入细节之前,让我们先定义一下我们的目标:
隔离性 - 我们希望确保每个测试都在隔离环境中运行。至少,这意味着每个测试应该有自己的数据库。这样可以确保测试不会相互干扰,并且您可以并行运行测试而不会出现任何问题。
性能 - 我们希望确保为测试设置的 PostgreSQL 是快速的。慢的解决方案会导致 CI/CD 中运行测试成本过高。我们提出的解决方案必须能够让我们执行测试而不会引入太多额外开销。
本文的其余部分将重点关注我们尝试过什么、有效的是什么,以及无法奏效的是什么。
不奏效的
使用事务
我们尝试的第一种方法是使用事务。我们会在每次测试开始时启动一个事务,并在结束时回滚它。
基本思想如下示例所示:
test('calculates total basket value', async () => { await pool.transaction(async (tx) => { await tx.query(sql.unsafe` INSERT INTO basket (product_id, quantity) VALUES (1, 2) `); const total = await getBasketTotal(tx); expect(total).toBe(20); }); });
事务处理方法在简单情况下效果很好(例如,测试单个函数),但当涉及测试多个组件之间的集成时,它很快就会成为一个问题。由于连接池、嵌套事务和其他因素,使事务处理方法正常工作所需的工作量意味着我们无法复制应用程序的真实行为,即不能提供我们需要的信心。
为了保持一致性,我们也希望避免混合测试方法。尽管对于某些测试来说使用事务就足够了,但我们希望在所有测试中都采用一致的方法。
使用 SQLite
我们尝试的另一种方法是使用 SQLite。SQLite 是一个快速且易于设置的内存数据库。与事务处理方式类似,对于简单情况,SQLite 运行良好。然而,在处理使用特定于 PostgreSQL 的功能代码路径时,它很快就会成为问题。在我们的情况下,由于使用了各种 PostgreSQL 扩展、PL/pgSQL 函数和其他特定于 PostgreSQL 的功能,我们无法将 SQLite 用于测试。
pglite 提供了将 PostgreSQL 打包为 WASM 模块,可在 Node.js 中使用。这可能是一个不错的选择,尽管我们还没有尝试过。无论如何,pglite 当前对扩展的支持不足会成为我们的障碍。
使用 pg_tmp
我们尝试的另一种方法是使用 pg_tmp。pg_tmp 是一个工具,为每个测试创建一个临时的 PostgreSQL 实例。从理论上讲,pg_tmp 是一个很好的解决方案。它允许完全隔离测试。在实践中,它比我们能够容忍的要慢得多。使用pg_tmp,启动和填充数据库需要几秒钟时间,并且在运行数千个测试时这些额外开销会迅速累积起来。假设您有1000 个测试,并且每个测试需要 1 秒钟才能运行。如果您为创建新数据库添加 2 秒钟的额外开销,则将增加额外2000 秒(33分钟)的开销量。
如果你喜欢这种方法,你也可能可以使用 Docker容器。情况不同,Docker 容器甚至可能比 pg_tmp 更快。 integresql 是我在 HN 帖子中遇到的一个项目。它似乎是一个很好的选择,可以将创建新数据库的开销减少到约500 毫秒。它具有一种池化机制,可以进一步减少开销。我们决定不继续沿着这条路走,因为我们对使用模板数据库获得的隔离水平感到满意。
奏效的方案
经过尝试各种方法后,我们决定结合两种方法:模板数据库(Template Databases)和挂载内存磁盘(mounting a memory disk)。这种方法使我们能够在数据库级别上隔离每个测试,而不会引入太多额外开销或复杂性。
模版数据库(Template Databases)
模板数据库是一个用作创建新数据库模板的数据库。当您从模板数据库创建新数据库时,新数据库具有与模板数据库相同的架构。从模板数据库创建新数据库的步骤如下:
- 创建一个模板数据库
ALTER DATABASE <database_name> is_template=true;
- 从模板数据库创建一个新的数据库
CREATE DATABASE <new_database_name> TEMPLATE <template_database_name>;
使用模板数据库的主要优势在于您无需处理管理多个 PostgreSQL 实例。您可以创建副本数据库,并使每个测试在隔离环境中运行。
然而,单独使用模板数据库对我们的用例来说并不够快。从模板数据库创建新数据库所需的时间仍然过长,无法满足运行数千次测试的需要:
postgres=# CREATE DATABASE foo TEMPLATE contra; CREATE DATABASE Time: 1999.758 ms (00:02.000)
所以就需要内存挂载出马了
需要注意的模板数据库的另一个限制是在复制过程中不能连接到源数据库的其他会话。如果在启动时存在任何其他连接,CREATE DATABASE 将失败;在复制操作期间,将阻止对源数据库的新连接。使用互斥模式可以很容易地解决这个限制,但这是需要注意的一点。
挂载内存磁盘
拼图的最后一块是挂载内存磁盘。通过挂载内存磁盘,并在内存磁盘上创建模板数据库,我们可以显著减少创建新数据库时的开销。
我将在下一节讨论如何挂载内存磁盘,但首先,让我们看看它会带来多大的差异。
postgres=# CREATE DATABASE bar TEMPLATE contra; CREATE DATABASE Time: 87.168 ms
这是一个重大的改进,使得这种方法对我们的使用场景变得可行。
不用说,这种方法并非没有缺点。数据存储在内存中,这意味着它不是持久化的。如果数据库崩溃或服务器重新启动,则数据会丢失。然而,对于运行测试来说,这并不是问题。每次创建新数据库时,数据都会从模板数据库重新生成。
使用 Docker 挂一个内存盘
我们采用的方法是使用一个带有内存磁盘的 Docker 容器。以下是设置步骤:
$ docker run \ -p 5435:5432 \ --tmpfs /var/lib/pg/data \ -e PGDATA=/var/lib/pg/data \ -e POSTGRES_PASSWORD=postgres \ --name contra-database \ --rm \ postgres:14
在上述命令中,我们正在创建一个 Docker 容器,并挂载一个内存磁盘到 /var/lib/pg/data。我们还设置了 PGDATA 环境变量为 /var/lib/pg/data,以确保 PostgreSQL 使用内存磁盘进行数据存储。最终结果是底层数据被存储在内存中,大大减少了创建新数据库的开销。
管理测试数据库
基本思路是在运行测试之前创建一个模板数据库,然后为每个测试从模板数据库创建一个新的数据库。以下是如何管理测试数据库的简化版本:
import { createPool, sql, stringifyDsn, } from 'slonik'; type TestDatabase = { destroy: () => Promise<void>; getConnectionUri: () => string; name: () => string; }; const createTestDatabasePooler = async (connectionUrl: string) => { const pool = await createPool(connectionUrl, { connectionTimeout: 5_000, // This ensures that we don't attempt to create multiple databases in parallel. maximumPoolSize: 1, }); const createTestDatabase = async ( templateName: string, ): Promise<TestDatabase> => { const database = 'test_' + uid(); await pool.query(sql.typeAlias('void')` CREATE DATABASE ${sql.identifier([database])} TEMPLATE ${sql.identifier([templateName])} `); return { destroy: async () => { await pool.query(sql.typeAlias('void')` DROP DATABASE ${sql.identifier([database])} `); }, getConnectionUri: () => { return stringifyDsn({ ...parseDsn(connectionUrl), databaseName: database, password: 'unsafe_password', username: 'contra_api', }); }, name: () => { return database; }, }; }; return () => { return createTestDatabase('contra_template'); }; }; const getTestDatabase = await createTestDatabasePooler();
这样您就可以使用 getTestDatabase 来为每个测试创建一个新数据库。destroy 方法可用于在测试运行后清理数据库。
结论
这种设置使我们能够在多个分片上并行运行数千个测试,而不会出现任何问题。创建新数据库的开销很小,并且隔离是在数据库级别进行的。我们对这种设置提供的性能和可靠性感到满意。
编者按
文中介绍的方案利用了 PostgreSQL 独有的模版库功能。MySQL 上没有,但思路可以借鉴。在 MySQL 上我们可以:
- 创建数据库
CREATE DATABASE new_database;
- 导出数据库
mysqldump -u username -p existing_database > backup.sql
- 导入到新数据库
mysql -u username -p new_database < backup.sql
另外也可以看一下 TestContainer 的方案,这个产品也已经被 Docker 收购了。
💡 更多资讯,请关注 Bytebase 公号:Bytebase

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
warm-flow 工作流发布 v1.1.90,新增 mybatis-plus 扩展,租户和逻辑删除支持配置
@TOC 欢迎使用使用warm-flow 更新记录 v1.1.80 orm支持mybatis-plus扩展 多租户字段隔离提供全局配置,自动获取 增加软删除可以配置化 新增三个测试模块 更新注意事项: 1、需要执行组件中的warm-flow_1.1.8.sql脚本,RuoYi-Vue-Warm-Flow项目只需要执行项目中的warm-flow_1.1.8.sql即可 本次更新特点 新增mybatis-plus扩展,非mysql外,只需转换表结构即可支持 非mysql系列的系统,也可以使用此组件 支持多租户和软删除(也可使用mybatis-plus自带的多租户插件和软删除配置) 多租户和软删除使用 # warm-flow工作流配置 warm-flow: # 是否显示banner图,默认是 banner: true # 填充器 (可配置文件注入,也可用@Bean/@Component方式) data-fill-handler-path: com.warm.flow.core.test.handle.CustomDataFillHandler # 全局租户处理器(可配置文件注...
- 下一篇
Sermant在异地多活场景下的实践
本文分享自华为云社区《Sermant在异地多活场景下的实践》,作者:华为云开源。 Sermant社区在1.3.0和1.4.0版本相继推出了消息队列禁止消费插件和数据库禁写插件,分别用于解决异地多活场景下的故障切流和保护数据一致性问题。本文将对Sermant在异地多活场景下的实践进行剖析。 一、异地多活 1.1 什么是异地多活 对于一个软件系统,我们希望当系统出现故障时仍然可以正常对外提供服务,软件系统的这种特性称之为高可用, 异地多活架构便是用来解决高可用问题的。 最早的系统架构一般为单机架构,当数据库出现故障时,可能会导致业务长时间中断。为了解决这一问题,数据库发展为由主库和从库组成,主库负责读和写操作,从库只提供读操作,主数据库的数据会实时同步至从数据库保持数据的一致性和完整性。当主库出现问题时,从库切换为主库继续工作。不过,这些服务都部署在同一机房甚至是同一个机柜,当机房出现故障后,系统仍然不能正常对外提供服务。 此时,同城双活成为很好的解决方案,在一个城市部署两个机房,两个机房部署相同的软件环境,并且均提供服务。当其中一个机房出现故障时,可以将流量切换至另一个机房继续执行,以保...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2整合Redis,开启缓存,提高访问速度
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- MySQL8.0.19开启GTID主从同步CentOS8
- Mario游戏-低调大师作品
- Linux系统CentOS6、CentOS7手动修改IP地址
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7安装Docker,走上虚拟化容器引擎之路
- Docker快速安装Oracle11G,搭建oracle11g学习环境