Dragonfly 系统测试指南:Pytest 单元测试与 Docker 客户端集成测试实战
【免费下载链接】dragonflyA modern replacement for Redis and Memcached项目地址: https://gitcode.com/GitHub_Trending/dr/dragonfly
本指南以 tests/README.md 为核心,系统讲解 Dragonfly(Redis / Memcached 的现代替代品)的两大测试体系:基于 Pytest 的 Python 系统测试,以及基于 Docker 的第三方客户端集成测试。读完本文,你将掌握如何定位dragonfly二进制、使用df_server/client/async_client/pool/async_pool等核心 fixture 编写测试,通过@dfly_args与@dfly_multi_test_args为实例注入启动参数,并用--gdb、--df、--repeat等命令行开关调试问题;同时学会如何构建 node-redis、ioredis、Jedis 等客户端容器镜像,在 Docker 中跑通跨生态的兼容性验证。
1. 测试体系总览
Dragonfly 仓库的测试分为两个层次,位于 tests/ 目录下:
- Pytest 系统测试(tests/dragonfly/):直接以子进程方式启动
dragonfly二进制,用 Python 客户端(redis、redis.asyncio、pymemcache等)对其发起真实命令,覆盖连接、快照、复制、集群、搜索、流式结构、发布订阅等几乎所有功能模块; - 集成测试(tests/integration/):为 node-redis、ioredis、Jedis 等第三方 Redis 客户端分别提供 Dockerfile,在容器内运行客户端官方测试套件来验证与 Dragonfly 的兼容性,Docker 容器默认假设 Dragonfly 运行在
localhost:6379。
此外,仓库还提供了丰富的辅助工具:fuzz/ 目录下的模糊测试种子与变异器、tests/dragonfly/seeder/ 中的数据填充器(Seeder)、tests/dragonfly/instance.py 中的实例管理类,以及 tests/dragonfly/proxy.py 的网络代理。系统测试框架本身遵循「share nothing」的隔离策略,力求每个测试互不产生副作用。
2. 环境准备:找到 dragonfly 二进制并安装 Python 依赖
2.1 二进制路径约定
Pytest 测试默认假定dragonfly二进制位于仓库根目录的build-dbg目录中,即<root>/build-dbg/dragonfly。这一逻辑可以在 tests/dragonfly/conftest.py 中看到:
path = os.environ.get("DRAGONFLY_PATH", os.path.join(scripts_dir, "../../build-dbg/dragonfly"))如果二进制放在其他位置,可以通过DRAGONFLY_PATH环境变量覆盖:
export DRAGONFLY_PATH=/path/to/your/dragonfly pytest -xv dragonfly2.2 Python 版本与虚拟环境
测试需要 Python 3。如果你的机器同时装有 Python 2 和 Python 3,务必显式调用python3:
python3 -m pytest -xv dragonfly官方建议使用 Python 虚拟环境。创建并激活后,安装 tests/dragonfly/requirements.txt 中声明的全部依赖:
python3 -m venv venv source venv/bin/activate pip3 install -r dragonfly/requirements.txt如果你偏好无需激活环境的项目管理方式,也可以使用uv(Astral 出品)。在tests目录下初始化一次项目本地环境:
uv venv uv run pytest -s dragonfly/connection_test.py依赖清单中值得关注的核心库包括:pytest>=7.1.2、pytest-asyncio==0.20.1(固定版本)、pytest-repeat(支持--repeat)、redis>=5.2.1、pymemcache、meta_memcache、prometheus_client(用于解析/metrics)、psutil(用于探测进程端口与日志)、aiohttp,以及用于对象存储测试的boto3、azure-storage-blob和用于搜索功能的redis-om、numpy、ml_dtypes等。pytest.ini中通过asyncio_mode=auto开启了异步模式,并默认addopts = -ra --emoji --showlocals -m "not large",即默认排除large标记的重型测试。
3. 运行 Pytest 系统测试
3.1 全量与选择性运行
在tests目录下运行全部测试:
pytest -xv dragonfly只运行名称中包含特定子串的测试(-k做子串过滤,这是 pytest 原生的选择机制):
pytest -xv dragonfly -k <substring>例如只跑connection_test.py中名为test_subscribe的用例,同时把 Dragonfly 的日志打到 stdout、开启dragonfly_connection模块的 vmodule 日志级别 2(便于观察连接内部流程):
pytest dragonfly/connection_test.py -s --df logtostdout --df vmodule=dragonfly_connection=2 -k test_subscribe3.2 自定义命令行参数(pytest 插件级选项)
测试框架在 conftest.py 中通过pytest_addoption注册了一系列自定义开关,其中与 README 直接对应的有:
| 选项 | 说明 |
|---|---|
--gdb | 所有实例都在 gdb 中启动,便于断点调试(gdb 启动较慢,等待超时会放宽) |
--df arg=val | 向所有 Dragonfly 实例传递自定义参数,可多次使用 |
--log-seeder file | 将最近测试中 seeder 产生的所有单数据库命令记录到指定文件,用于回放复现 |
--existing-port | 不新起实例,而是连接一个已运行的 Dragonfly 进程端口来跑测试 |
--rand-seed | 设置全局随机种子,使 seeder 的数据生成可预测、可复现 |
--repeat <N> | 将每个测试重复运行 N 次(配合pytest-repeat,便于定位偶发失败) |
--repeat的实现方式很有参考价值:pytest_generate_tests钩子会向metafunc.fixturenames追加一个tmp_ctfixture,再用metafunc.parametrize("tmp_ct", range(count))将每个测试函数参数化展开,从而在不修改任何测试代码的前提下实现「重复运行」。(conftest.py)
除 README 提到的选项外,conftest 还提供了--existing-admin-port、--existing-mc-port(连接已运行的 admin / memcached 端口)、--direct-out(不后处理 Dragonfly 输出)、--buffered-output(缓冲实例输出便于分组)、--drop-data-after-each-test(在--repeat循环中每次测试后即清理数据,避免磁盘被撑满)等进阶开关。
4. 核心 fixture:与 Dragonfly 实例交互
所有 fixture 都定义在 tests/dragonfly/conftest.py 中,并且通过 tests/dragonfly/init.py 的dfly_args/dfly_multi_test_args装饰器与df_factory参数化机制联动。
4.1 五个基础 fixture
df_server:默认的 Dragonfly 实例,测试中直接可用。它由df_factory创建并start(),在df_factory(class 作用域)内共享,因此相同参数配置的测试会复用同一个实例;client与async_client:连接到默认实例的同步 / 异步客户端。每个新的 client 都会先flushall()清空实例,保证测试间数据隔离(见cluster_client、async_client的实现:都先flushall再select(DATABASE_INDEX),conftest.py);pool与async_pool:连接到默认实例的客户端连接池。async_pool构造为aioredis.ConnectionPool,max_connections=32,decode_responses=True,测试结束会disconnect(inuse_connections=True)断开全部连接(conftest.py)。
4.2 实例默认参数(从源码确认)
从 instance.py 的DflyInstanceFactory.create()可以看到每个测试实例默认携带的启动参数,这对理解测试行为很有帮助:
dbfilename="":默认不落盘,避免测试间相互污染;noversion_check:跳过版本检查;maxmemory=8G(macOS 不会自动设置,必须显式给出);vmodule=dragonfly_connection=1,db_slice=1,listener_interface=1,...:默认开启一组关键模块的详细日志;log_dir指向该测试类的日志目录(/tmp/dragonfly_logs/<class>,见df_log_dirfixture);num_shards若未显式指定,会默认设为proactor_threads - 1(当线程数 > 1 时),以便暴露更多分片相关的并发缺陷;- 若未指定
port,实例会以--port=-1启动,让 Dragonfly 自己挑选随机空闲端口,测试框架再用psutil探测实际监听端口(get_port_from_psutil)。
DflyInstance还提供port/admin_port/mc_port属性(对应--port、--admin_port、--memcached_port参数),以及metrics()(抓取并解析http://localhost:<port>/metrics的 Prometheus 指标)、find_in_logs(pattern)(实例停止后按正则检索日志文件)、rss(当前常驻内存)等调试辅助方法。
4.3 df_seeder_factory:可复现的数据填充器
df_seeder_factoryfixture 从--rand-seed读取种子;若未提供,则取random.randrange(sys.maxsize)生成随机种子并打印到日志,便于复现失败(conftest.py)。--log-seeder则会把 seeder 产生的命令记录到文件,配合--rand-seed可以精确回放数据生成过程。
5. 编写测试:装饰器、发现规则与实例
5.1 测试发现规则
pytest 会递归扫描tests/dragonfly目录,匹配test_*.py或*_test.py文件名,以及以下函数/方法命名规则:
- 类外以
test前缀命名的函数; - 类内以
test前缀命名、且类名以Test开头(类不允许定义__init__方法)的方法。
注意:在tests/dragonfly下新建子目录时,必须创建__init__.py文件,否则可能出现同名模块冲突(pytest 官方建议的「tests outside application code」最佳实践)。Dragonfly 的做法是在包内直接提供共享工具,例如 tests/dragonfly/init.py 中的dfly_args、dfly_multi_test_args与PortPicker。
5.2 用 @dfly_args 传递单一参数配置
@dfly_args用于给当前测试(或测试类)指定一组Dragonfly 启动参数。它本质上是对df_factoryfixture 的pytest.mark.parametrize(..., indirect=True)参数化封装(init.py)。示例:单线程实例下验证事务队列不会乱序执行:
@dfly_args({"proactor_threads": 1}) async def test_txq_ooo(async_client: aioredis.Redis, df_server): ...见 tests/dragonfly/generic_test.py。
5.3 用 @dfly_multi_test_args 测试多组配置
@dfly_multi_test_args允许一次指定多组参数配置,每一组配置会创建一个独立的 Dragonfly 实例,测试函数分别拿到各自实例的客户端。例如验证keys_output_limit取 512 与 1024 两种配置下的 KEYS 截断行为:
@dfly_multi_test_args({"keys_output_limit": 512}, {"keys_output_limit": 1024}) class TestKeys: async def test_max_keys(self, async_client: aioredis.Redis, df_server): max_keys = df_server["keys_output_limit"] pipe = async_client.pipeline() batch_fill_data(pipe, gen_test_data(max_keys * 3)) await pipe.execute() keys = await async_client.keys() assert len(keys) in range(max_keys, max_keys + 512)见 tests/dragonfly/generic_test.py。注意df_server["keys_output_limit"]这种下标访问方式——DflyInstance.__getitem__会返回实例实际使用的启动参数值(instance.py)。
5.4 参数中的环境变量插值
装饰器参数支持格式化字符串:"{<VAR>}"会被替换为环境变量<VAR>的值。由于 pytest 目前的限制,fixture 不能直接传入装饰器,因此这是把临时目录路径等动态值传入 CLI 参数的官方推荐方式。在 snapshot_test.py 中可以看到典型用法:
BASIC_ARGS = {"dir": "{DRAGONFLY_TMP}/", "proactor_threads": 4}这里的{DRAGONFLY_TMP}来自test_envfixture——它会把临时目录注入环境变量DRAGONFLY_TMP(conftest.py),随后DflyInstanceFactory.create()中args[k].format(**self.params.env)完成替换(instance.py)。
5.5 三个参考测试文件
- snapshot_test.py:综合演示
@dfly_args、环境变量插值以及测试前准备(如通过BASIC_ARGS指定dir、构造 Azurite/MinIO 对象存储环境、用 seeder 灌数据后做SAVE并校验快照文件); - generic_test.py:演示
@dfly_multi_test_args多配置测试,以及通过df_factory.create(...)在测试内按需创建带特定参数(如requirepass)的独立实例; - connection_test.py:多异步连接并发场景的测试范例,包含
CollectingMonitor这类借助MONITOR命令采集服务端消息、并利用ECHO标记同步就绪状态的实现,还针对enable_resp_io_loop_v2新 IO 循环做了能力探测(is_resp_io_loop_v2)。
5.6 编写自己的 fixture
所有 fixture 都集中在 conftest.py 中。新增 fixture 前先确认是否已有现成实现;新 fixture 的作用域应尽量小,保证测试彼此独立、无副作用——这正是仓库奉行的「share nothing」策略(与 docs/df-share-nothing.md 的设计理念一脉相承)。
5.7 管理依赖
新增依赖必须同步到 tests/dragonfly/requirements.txt。可以在tests/dragonfly目录下用以下命令生成完整清单:
pip3 freeze > requirements.txt6. 集成测试:在 Docker 中跑第三方客户端
集成测试位于 tests/integration/ 目录。每个被测客户端包提供一个独立的 Dockerfile,容器内包含运行其官方测试套件所需的全部环境;测试时假设 Dragonfly 已在本机localhost:6379上运行。通用运行方式:
docker build -t [test-name] -f [test-dockerfile-name] . docker run --network=host [test-name]使用--network=host是为了让容器直接访问宿主机上监听 6379 端口的 Dragonfly 实例。
6.1 node-redis
针对 node-redis 客户端的集成测试(构建文件:tests/integration/node-redis.Dockerfile)。该 Dockerfile 基于node:18.7.0,克隆 Dragonfly 维护的 node-redis fork 分支,构建测试工具后以npm run test -w ./packages/client -- --redis-version=2.8作为启动命令。
构建与运行:
docker build -t node-redis-test -f ./node-redis.Dockerfile . docker run --network=host node-redis-test只跑选定测试时,可借助-g <regex>按 mocha 的 grep 规则过滤(--redis-version指定协议版本,这里为 2.8):
docker run --network=host node-redis-test npm run test -w ./packages/client -- --redis-version=2.8 -g <regex>mocha 框架的其他命令行选项也可以按此方式追加传入。
6.2 ioredis
ioredis 是 Node.js 生态中性能导向、功能全面的 Redis 客户端,自带非常庞大的测试覆盖。目前 Dragonfly 尚未支持其全部特性,因此 README 明确不要脱离 Docker 镜像直接运行 ioredis 测试——镜像会锁定正确版本并对部分用例打补丁。
官方推荐通过脚本 run_ioredis_on_docker.sh 运行。如果镜像已经构建好,直接执行:
./integration/run_ioredis_on_docker.sh更稳妥的做法是加--build,先重新构建(或确保)镜像再执行测试:
./integration/run_ioredis_on_docker.sh --build也可以手动构建镜像后单独运行:
docker build -t ioredis-test -f ./ioredis.Dockerfile . docker run --rm -i --network=host ioredis-test ./run_tests.sh脚本会执行docker run --rm -i --network=host ioredis-test ./run_tests.sh,其中run_tests.sh是由ADD .run_ioredis_valid_test.sh run_tests.sh注入的受限测试清单(ioredis.Dockerfile),因此只运行当前 Dragonfly 已支持的用例,未支持的(如集群、Elasticache 相关)会被跳过。关于 Dockerfile 中 ENTRYPOINT 与 ioredis 项目package.json里 npm test 脚本的对应关系,可对照两者源码理解。
需要说明的背景:目前 ioredis 测试中 MONITOR 命令用例会失败,因为 Dragonfly 始终以大写返回命令名,而测试期望小写,这也是用打补丁镜像运行的原因之一。
6.3 Jedis
Jedis 集成测试的构建文件是 tests/integration/jedis.Dockerfile:基于maven:3.8.6-jdk-11,克隆 Dragonfly 维护的 jedis fork 分支,先用mvn test -DskipTests完成编译,启动命令则通过mvn surefire:test -Dtest=...只执行一组选定测试类(覆盖各类值命令、位命令、控制命令、哈希、列表、脚本、集合、事务、客户端、发布订阅、有序集合、排序与流命令)。
构建与运行:
docker build -t jedis-test -f ./jedis.Dockerfile . docker run --network=host jedis-test7. 常见调试技巧与故障排查
- 定位二进制:确认
DRAGONFLY_PATH指向有效的 dragonfly 可执行文件,否则会报「Failed to start instance」。若使用--existing-port,测试框架将直接复用该端口上的进程,不再启动新实例(此时start()直接返回,见 instance.py); - 复现偶发失败:
--rand-seed固定随机种子 +--log-seeder file记录 seeder 命令,再配合--repeat <N>反复执行以放大问题; - 观察服务端日志:
--df logtostdout --df vmodule=<module>=<level>把日志打到 stdout 并针对某模块提高日志级别。实例日志默认写入/tmp/dragonfly_logs/<测试类名>/,测试失败时框架会把日志复制到/tmp/failed/便于归档分析(见copy_failed_logs与pytest_runtest_makereport钩子,conftest.py); - gdb 调试:
--gdb让实例在 gdb 内启动;若进程无法优雅终止,框架会发送SIGUSR1触发 Dragonfly 打印栈,随后自动addr2line符号化栈地址(见DflyInstance.stop()与symbolize_stack_trace,instance.py); - S3 / 对象存储相关测试:conftest 支持
MINIO_S3_ENDPOINT环境变量——设置后会自动下载并拉起一个本地 MinIO 服务(若网络允许),创建dragonfly-testbucket,并把DRAGONFLY_S3_BUCKET、AWS_*等环境变量注入测试进程,同时为 Dragonfly 实例追加s3_endpoint相关启动参数(conftest.py、instance.py)。该能力服务于snapshot_test.py中的 S3 快照上传下载用例; - TLS 测试:conftest 提供
with_tls_server_args、with_ca_tls_server_args、with_tls_client_args等 fixture,用gen_ca_cert/gen_certificate现场生成 CA 与服务器/客户端证书,覆盖 TLS 加密与双向认证场景(conftest.py)。
8. 测试标记与 CI 约定
tests/pytest.ini 中定义了多个自定义 marker,写测试时可按需使用,CI 也会据此分流:
opt_only:仅适合在优化(release)构建下运行的测试(如复制压力测试),CI 的 regression/release 工作流才会执行;exclude_epoll:已知在 epoll 事件循环下失败的测试,epoll 工作流会跳过;debug_only:仅适合 debug 构建(release 构建太快导致断言无法触发);large:重型测试,需要大规格 CI runner,默认通过-m "not large"排除;replication:配置replicationfixture 的拓扑(如master_args、replica_args、replicas),关键字参数会转发给 replication_utils.py 的setup_replication。
replicationfixture 本身也支持按需切换拓扑,例如在复制一致性测试中动态指定主从参数,从而覆盖从单机到主从、再到集群的多形态部署验证。
9. 小结
本文完整梳理了 Dragonfly 的两级测试体系:以 conftest.py 为核心的 Pytest 系统测试(二进制定位、五个基础 fixture、装饰器参数注入、可复现 seeder、失败日志归档),以及以 Docker 为边界的第三方客户端集成测试(node-redis / ioredis / Jedis 的构建与运行)。无论你是想为 Dragonfly 提交一个新功能并补齐测试,还是想在自己的环境中复现某个用例,都可以直接参考 tests/README.md 与本仓库 tests/ 下的真实实现开始动手。
【免费下载链接】dragonflyA modern replacement for Redis and Memcached项目地址: https://gitcode.com/GitHub_Trending/dr/dragonfly
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考