Dragonfly 系统测试指南:Pytest 单元测试与 Docker 客户端集成测试实战
2026/9/11 5:30:53 网站建设 项目流程

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 客户端(redisredis.asynciopymemcache等)对其发起真实命令,覆盖连接、快照、复制、集群、搜索、流式结构、发布订阅等几乎所有功能模块;
  • 集成测试(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 dragonfly

2.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.2pytest-asyncio==0.20.1(固定版本)、pytest-repeat(支持--repeat)、redis>=5.2.1pymemcachemeta_memcacheprometheus_client(用于解析/metrics)、psutil(用于探测进程端口与日志)、aiohttp,以及用于对象存储测试的boto3azure-storage-blob和用于搜索功能的redis-omnumpyml_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_subscribe

3.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 作用域)内共享,因此相同参数配置的测试会复用同一个实例
  • clientasync_client:连接到默认实例的同步 / 异步客户端。每个新的 client 都会先flushall()清空实例,保证测试间数据隔离(见cluster_clientasync_client的实现:都先flushallselect(DATABASE_INDEX),conftest.py);
  • poolasync_pool:连接到默认实例的客户端连接池。async_pool构造为aioredis.ConnectionPoolmax_connections=32decode_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_argsdfly_multi_test_argsPortPicker

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.txt

6. 集成测试:在 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-test

7. 常见调试技巧与故障排查

  • 定位二进制:确认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_logspytest_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_BUCKETAWS_*等环境变量注入测试进程,同时为 Dragonfly 实例追加s3_endpoint相关启动参数(conftest.py、instance.py)。该能力服务于snapshot_test.py中的 S3 快照上传下载用例;
  • TLS 测试:conftest 提供with_tls_server_argswith_ca_tls_server_argswith_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_argsreplica_argsreplicas),关键字参数会转发给 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询