Neo4j Cypher Shell 交互式集成测试:用 Expect 脚本与 Docker 驱动端到端验证
2026/9/21 15:55:20 网站建设 项目流程
  • 数据库
  • 图数据库
  • 后端

【免费下载链接】neo4j

Graphs for Everyone

项目地址:https://gitcode.com/gh_mirrors/ne/neo4j
点击查看免费下载

Cypher Shell 是 Neo4j 自带的命令行客户端,它的交互行为(提示符、历史命令、Ctrl+C 中断、空闲超时、参数设置)极其依赖真实终端环境。本文以 Neo4j 仓库中integration-test-expect模块为骨架,完整讲解如何用 Expect 脚本在 Docker 容器中驱动 Cypher Shell 运行、用配套.expected文件断言完整交互过程,并深入源码解析 testcontainers 的容器编排、交互清洗与动态值替换机制。读完本文,你将掌握该模块的测试编写规范、运行命令与底层实现原理,可以直接仿照新增自己的交互测试场景。

模块定位与工作方式

在 Neo4j 仓库中,community/cypher-shell/integration-test-expect是一个独立的集成测试模块,其定位在 integration-test-expect/README.md 中有明确说明:它使用 testcontainers 自行管理依赖(即 Neo4j 数据库与 Expect 运行环境两个 Docker 容器),专门用于对 Cypher Shell 做端到端的交互式验证。

测试脚本统一存放在 expect/tests/README.md,该 README 定义了本模块的两条核心规则:

  1. 每个.expect文件都将在 Docker 容器中运行——Expect 脚本不在宿主机上直接执行,而是被复制进一个专门构建的容器镜像,由该容器内的 expect 解释器运行;
  2. 每个.expect文件必须有一个配套的.expected文件——后者是纯文本,逐字记录期望的完整终端交互内容,用于与真实输出做严格比对。

这条"脚本 + 期望输出"的一一对应约定,是整个模块的组织基石:.expect描述"怎么操作",.expected描述"应该看到什么"。

测试资源目录结构

expect测试资源分为两个子目录,各有分工:

  • community/cypher-shell/integration-test-expect/src/test/resources/expect/docker/:存放用于构建 Expect 容器的Dockerfile
  • community/cypher-shell/integration-test-expect/src/test/resources/expect/tests/:存放全部.expect脚本与对应的.expected文件,以及公共脚本common.expect

当前仓库中expect/tests/下已包含如下测试场景(每个场景都是.expect.expected成对出现,另有公共文件common.expect):

场景文件验证目标
return-1.expect基础查询执行与:exit退出
arrow-up-history.expect终端方向键上翻历史命令并重新执行
sigint.expect发送 Ctrl+C(\x03)中断后 Shell 仍可继续使用
cancel-query.expect取消一个长时间运行的查询(依赖 APOC 的apoc.util.sleep
timeout.expect--idle-timeout触发后的超时行为与提示文案
timeout-do-not-timeout.expect缓慢输入期间不触发空闲超时
parameters-arrow.expect:param a => 1箭头语法设置参数
parameters-multiline.expect:param {\n...\n}多行 JSON 语法设置参数
parameters-multiline-complex.expect多行 JSON 参数中带表达式与duration()构造

如何运行集成测试

集成测试默认是关闭的。模块的 pom.xml 通过enable-cypher-shell-integration-testprofile 在未显式开启时跳过 surefire 与 failsafe 测试,同时在<description>中注明需要通过-DenableCypherShellIntegrationTest开启。运行方式见 integration-test-expect/README.md:

mvn integration-test --projects org.neo4j:cypher-shell-integration-test-expect -DenableCypherShellIntegrationTest

也可以直接从 IDE 中运行继承ExpectTestBase的测试类。运行前提包括:本机可用 Docker(testcontainers 需要拉取并启动 Neo4j 与 Expect 两个容器),以及先构建出 Cypher Shell 发行包(cypher-shell.zip,见下文源码分析)。

如何新增一个测试场景

遵循 expect/tests/README.md 与模块 README 的步骤,新增场景只需三步:

  1. community/cypher-shell/integration-test-expect/src/test/resources/expect/tests/下创建 Expect 脚本,例如my-scenario.expect
  2. 创建同名文本文件并追加.expected后缀,即my-scenario.expect.expected,内容为该场景期望的完整交互输出;
  3. 运行任意继承ExpectTestBase的测试类——参数化测试会自动扫描expect/tests/目录下的所有.expect文件并逐个执行,无需改动 Java 代码。

需要注意的是,ExpectTestBase.allExpectResources()(见 ExpectTestBase.java)在扫描时会过滤掉common.expect,且要求至少找到 2 个测试用例,否则直接抛异常。

容器编排与执行流程源码解析

真正把"每个.expect在 Docker 容器中运行"落地的是 ExpectTestExtension.java,它是一个 JUnit 5 的BeforeAllCallback/AfterAllCallback扩展,整个流程可拆解为四步。

1. 构建 Expect 镜像(Dockerfile)

镜像通过ImageFromDockerfile动态构建,Dockerfile 位于 expect/docker/Dockerfile:

FROM eclipse-temurin:17-jre-alpine RUN apk --no-cache add expect unzip COPY cypher-shell.zip cypher-shell.zip RUN unzip cypher-shell.zip RUN mv cypher-shell-* cypher-shell COPY expect/* / ENTRYPOINT ["tail", "-f", "/dev/null"]

要点:基础镜像带 JRE 17(与 Cypher Shell 的 Java 运行需求匹配),通过apk安装 expect 与 unzip;构建时把cypher-shell.zip(来自../cypher-shell/target/目录,见cypherShellZip()方法)解压为/cypher-shell,并把全部.expect脚本复制到镜像根目录;入口用tail -f /dev/null保持容器存活,等待execInContainer注入命令。

2. 启动两个容器

createAndStartContainers()创建共享 Docker 网络Network.newNetwork(),然后启动:

  • Neo4j 容器NeoContainer("neo4j:" + neo4jDockerTag),通过withNetworkAliases("neo4j")设置网络别名,管理员密码为123techno,并挂载 APOC 插件(Neo4jLabsPlugin.APOC,供cancel-query场景使用);
  • Expect 容器GenericContainer,注入四个环境变量:NEO4J_USER=neo4jNEO4J_PASSWORD=123technoNEO4J_ADDRESS=neo4jCYPHER_SHELL_PATH=/cypher-shell/bin/cypher-shell

3. 执行脚本并断言

runTestCase()先根据.expect路径推导出同名.expected资源路径,若缺失则直接抛RuntimeException("Missing expected output file: ...")。随后在 Expect 容器内执行:

expectContainer.execInContainer(new String[] {"expect", expectScriptFilename});

若退出码非 0 或 stderr 非空,测试失败并打印完整的 stdout/stderr 诊断信息;否则调用assertEqualInteraction()将实际 stdout 与期望文本比对。

4. 清理

afterAll()依次关闭 Neo4j 容器与 Expect 容器。

公共脚本 common.expect 详解

common.expect(见 tests/common.expect)是所有测试场景复用的公共基础设施,值得逐段理解:

  • 环境变量注入:读取CYPHER_SHELL_PATHNEO4J_ADDRESSNEO4J_USERNEO4J_PASSWORD四个环境变量;
  • 默认配置timeout设为 20 秒;stty_init把终端尺寸设为rows 1000 cols 200send_human用于模拟人类输入节奏;
  • 辅助过程
    • sendQuery { query }:等待neo4j@neo4j提示符后再等待>输入提示,然后发送查询内容并回车;
    • expectPrompt {}:仅等待提示符出现;
    • expectCleanExit {}:等待Bye!字样并期待进程eof,从而确认 Cypher Shell 正常退出;
  • 启动与兜底:通过spawn启动$cypher_shell_path,参数为-a $neo4j_address -u $neo4j_user -p $neo4j_password,并支持测试通过extra_args追加自定义参数(如--idle-timeout);expect_after统一处理eof(输出Error: Unexpected EOF,退出码 1)与timeout(输出Error: Timeout!,退出码 2),任何脚本都无需重复处理这两类异常。

正是借助extra_args机制,timeout.expect才能在source common.expect之前用set extra_args {"--idle-timeout" "1s" "--hidden-idle-timeout-delay" "1s"}为 Cypher Shell 注入超时参数,从而验证"空闲 1 秒即超时退出"的交互文案。

交互断言:清洗动态内容与 ANSI 码

.expected文件记录的是"理想交互",但真实输出中混有 Bolt 版本号、毫秒级耗时、终端 ANSI 转义序列等不稳定内容,直接逐字比对必然失败。ExpectTestExtension.java 中的InteractionAssertion通过两级清洗解决:

  1. 正则替换动态值REPLACEMENT_PATTERNS):把Connected to Neo4j using Bolt protocol version ([0-9.]+) atready to start consuming query after ([0-9]+) ms, results consumed after another ([0-9]+) ms中的版本号、毫秒数替换为<removed>,使不同 Neo4j 版本、不同机器负载下的输出都可稳定断言;
  2. 剥离 ANSI 码与无关行removeAnsiCodes):用\u001B\[[?]?[;\\d]*[mhlCDA]模式去掉终端着色与控制序列(源码注释说明:可以断言 ANSI 码,但手写这些断言毫无乐趣),并过滤掉spawn /cypher-shell/bin/cypher-shell这一行 expect 自身的输出,最后对每行做trim

这也解释了arrow-up-history.expect.expected中出现的奇特内容:脚本连续发送三次上方向键\033[A\033[A\033[A,终端先回显查询又逐字擦除改写,因此期望文件里出现了return 3 as result;21这样的行(源码注释明确说明:额外的21是 Cypher Shell 在按上翻键改写查询时产生的输出痕迹)。这正是"脚本 + 期望文本"这一机制的精细化体现。

典型测试场景逐例解读

基础查询:return-1

return-1.expect 是最简单的完整流程:sendQuery "return 1 as result;"sendQuery ":exit"expectCleanExit。其 期望输出 完整记录了欢迎语、Bolt 连接信息、+--------+边框 ASCII 表格、1 row统计与Bye!退出语,可作为新场景的模板参照。

命令历史上翻:arrow-up-history

连续执行三次return N as result;后发送三次上方向键(\033[A),应回溯到最近一次执行的return 3 as result;;再按一次上方向键则回退到return 1 as result;。该场景验证了 Cypher Shell 的 Readline 风格历史导航在真实终端下工作正常,同时也展示了期望文件中如何处理终端逐字编辑产生的杂讯。

中断信号:sigint

sigint.expect 在输入提示符处发送\x03(Ctrl+C),期望输出显示Interrupted (...)提示,且随后return 2 as result;仍能正常执行——验证中断不会破坏 Shell 会话。

取消长查询:cancel-query

cancel-query.expect 启动一个依赖 APOC 的长查询unwind range(0,90000) as x call apoc.util.sleep(1) return sum(x) as sum;,将timeout临时调至 60 秒,随后发送\x03,断言出现Stopping query...terminated字样;若只看到提示符而未收到终止消息,脚本会输出Error: Missing error message并以退出码 1 失败。最后再执行一次普通查询确认会话可用。该场景覆盖了 Cypher Shell 的查询取消与错误提示路径。

空闲超时:timeout 与 timeout-do-not-timeout

  • timeout.expect:以--idle-timeout 1s --hidden-idle-timeout-delay 1s启动,等待Timeout after idling提示后期待eof;期望输出完整给出了提示文案:Timeout after idling, avoid this by increasing --idle-timeout or omitting it completely.
  • timeout-do-not-timeout.expect:以--idle-timeout 5s --hidden-idle-timeout-delay 0.2s启动,然后模拟人类逐字符缓慢输入return 1 as result;(每字符间隔 0.5 秒,总计约 10 秒),验证"只要持续有输入就不算空闲"的语义。源码注释也坦诚标注该测试有较高的 flaky 风险,必要时需调大--idle-timeout

参数设置:parameters 系列

三个场景分别覆盖 Cypher Shell 的两种:param用法:

  • parameters-arrow.expect:箭头语法:param a => 1、带分号:param b => 2;,随后:param列出参数、return $a, $b;使用参数;
  • parameters-multiline.expect\r分隔的多行 JSON 形式:param {\ra: 1,\rb: 2\r}
  • parameters-multiline-complex.expect:多行 JSON 中携带表达式1 + 2 * 4duration({seconds:1}) + duration({hours:1}),验证参数值支持求值表达式与时间类型构造。

三者期望输出中:param列出的参数均为缩进的 JSON 块,验证了两种参数语法结果等价。

版本矩阵与测试基类

integration-test-expect的测试类按 Neo4j 版本划分,均继承ExpectTestBase(后者以 JUnit 5@ParameterizedTest+@MethodSource("allExpectResources")对每个.expect资源执行一次场景测试):

测试类目标 Neo4j 版本
CypherShellEnterpriseIntegrationTest.javaenterprise(最新)
CypherShellEnterprise51IntegrationTest.java5.1-enterprise
CypherShellEnterprise44IntegrationTest.java4.4-enterprise

各测试类仅通过@RegisterExtension注册不同 Docker 镜像 tag 的ExpectTestExtension,同一套.expect场景即可在不同版本上回归验证。这也解释了为何InteractionAssertion必须把 Bolt 协议版本号正则替换掉——同一脚本要同时兼容 4.4(Bolt 4)与 5.x(Bolt 5.1)的欢迎语。

小结

Neo4j 的 Cypher Shell 交互测试遵循一套简洁而严谨的约定:expect/tests/下每个.expect脚本描述一次真实终端会话的操作序列,配套的.expect.expected文件记录期望的完整交互,二者缺一不可;运行时由 testcontainers 拉起 Neo4j 与 Expect 两个容器,脚本在容器内驱动 Cypher Shell 执行,Java 侧对输出做 ANSI 清洗与动态值归一化后与期望文本严格比对。无论是验证历史导航、Ctrl+C 中断、查询取消、空闲超时还是参数语法,这套机制都无需额外的 Java 测试代码——写好脚本与期望文本即可获得端到端的交互回归保障。仓库中的 expect/tests/ 目录本身就是最佳范例库,新增场景时直接参照return-1.expect的最小骨架,再按需复用common.expect的辅助过程即可。

  • 数据库
  • 图数据库
  • 后端

【免费下载链接】neo4j

Graphs for Everyone

项目地址:https://gitcode.com/gh_mirrors/ne/neo4j
点击查看免费下载
上一篇:Transformer Engine CUDA内核优化:深入理解高性能计算实现原理
下一篇:5分钟上手Xamarin async/await:mobile-samples中的异步编程实例

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询