- 数据库
- 图数据库
- 后端
【免费下载链接】neo4j
Graphs for Everyone
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 定义了本模块的两条核心规则:
- 每个
.expect文件都将在 Docker 容器中运行——Expect 脚本不在宿主机上直接执行,而是被复制进一个专门构建的容器镜像,由该容器内的 expect 解释器运行; - 每个
.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 的步骤,新增场景只需三步:
- 在
community/cypher-shell/integration-test-expect/src/test/resources/expect/tests/下创建 Expect 脚本,例如my-scenario.expect; - 创建同名文本文件并追加
.expected后缀,即my-scenario.expect.expected,内容为该场景期望的完整交互输出; - 运行任意继承
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=neo4j、NEO4J_PASSWORD=123techno、NEO4J_ADDRESS=neo4j、CYPHER_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_PATH、NEO4J_ADDRESS、NEO4J_USER、NEO4J_PASSWORD四个环境变量; - 默认配置:
timeout设为 20 秒;stty_init把终端尺寸设为rows 1000 cols 200;send_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通过两级清洗解决:
- 正则替换动态值(
REPLACEMENT_PATTERNS):把Connected to Neo4j using Bolt protocol version ([0-9.]+) at与ready to start consuming query after ([0-9]+) ms, results consumed after another ([0-9]+) ms中的版本号、毫秒数替换为<removed>,使不同 Neo4j 版本、不同机器负载下的输出都可稳定断言; - 剥离 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 * 4与duration({seconds:1}) + duration({hours:1}),验证参数值支持求值表达式与时间类型构造。
三者期望输出中:param列出的参数均为缩进的 JSON 块,验证了两种参数语法结果等价。
版本矩阵与测试基类
integration-test-expect的测试类按 Neo4j 版本划分,均继承ExpectTestBase(后者以 JUnit 5@ParameterizedTest+@MethodSource("allExpectResources")对每个.expect资源执行一次场景测试):
| 测试类 | 目标 Neo4j 版本 |
|---|---|
| CypherShellEnterpriseIntegrationTest.java | enterprise(最新) |
| CypherShellEnterprise51IntegrationTest.java | 5.1-enterprise |
| CypherShellEnterprise44IntegrationTest.java | 4.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
相关推荐
KLayout测试套件:如何为EDA工具编写有效的单元测试
KLayout测试套件:如何为EDA工具编写有效的单元测试 KLayout是一款强大的开源EDA(电子设计自动化)工具,专门用于集成电路版图设计。在复杂的EDA
硬件开发桌面应用图形学终极美化指南:自定义hexo-theme-diaspora主题外观与功能
终极美化指南:自定义hexo theme diaspora主题外观与功能 hexo theme diaspora是一款简洁且响应式的Hexo博客主题,通过简单的
USD-Cookbook调试秘籍:解决USD开发中的常见问题
USD Cookbook调试秘籍:解决USD开发中的常见问题 USD(Universal Scene Description)作为强大的3D场景描述框架,在开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考