在上篇文章中,我们介绍了 Nebula Graph 的集成测试的演进过程。本篇就介绍一下向测试集合中添加一个用例,并成功运行所有的测试用例的过程。
在构建 2.0 测试框架之初,我们定制了部分工具类来帮助测试框架快速地启停一个单节点的 nebula 服务,其中有检查端口冲突、修改部分配置选项等功能。原来的执行流程如下:
pytest.main
并发执行所有的测试用例;其中的不便之处在于,当需要给 pytest 指定某些参数选项时,需要将该参数透传给pytest.main
函数,并且每次运行单个测试用例需要通过cmake
生成的脚本来操作,不是很方便。我们希望 “测试用例在哪儿,就在哪儿执行测试”。
在本次测试框架的改造过程中,我们除了改变了程序入口之外,大部分复用了原来封装好的逻辑。由于 nebula 目前积累了很多的用例,单进程运行已经不能满足快速迭代的需求,在尝试了其他并行插件之后,考虑到兼容性,我们最终选择了 pytest-xdist 插件来加速整个测试流程。
但是 pytest 只提供了四种 scope 的 fixture:session,module,class 和 function。而我们希望能用一种 global 级别的 fixture 来完成 nebula 服务的启动和初始化。目前最高层次的 session 级别还是每个 runner 都要执行一次,如此如果有 8 个 runner 的话,就要启动 8 个 nebula 服务,这不是我们期望的。
参考 pytest-xdist 的文档,需要通过文件锁来进行不同 runner 之间的并行控制。为了让控制逻辑足够的简单,我们把程序启停和预备的逻辑同执行测试的过程分开,使用单独的步骤控制 nebula 的启动,当某些测试有问题时,还可以通过 nebula-console 单独连接测试的服务,进行进一步的验证调试。
在此之前,Nebula 的数据导入过程是直接执行一条拼接好的 nGQL INSERT 语句。这样做,存在如下问题:
针对以上的问题,参考nebula-importer的实现,我们将导入的逻辑和数据集完全分离,重新实现了 python 版的导入模块。不过,目前只支持导入 csv 类型的数据文件,且每个 csv 文件中只能存储一个tag/edge
类型。
重构导入的逻辑之后,目前 nebula 的测试数据集变得清晰明了:
nebula-graph/tests/data
├── basketballplayer
│ ├── bachelor.csv
│ ├── config.yaml
│ ├── like.csv
│ ├── player.csv
│ ├── serve.csv
│ ├── team.csv
│ └── teammate.csv
├── basketballplayer_int_vid
│ └── config.yaml
└── student
├── config.yaml
├── is_colleagues.csv
├── is_friend.csv
├── is_schoolmate.csv
├── is_teacher.csv
├── person.csv
├── student.csv
└── teacher.csv
3 directories, 16 files
每个目录包含一个 space 中所有的 csv 数据文件,通过该目录下的config.yaml
来配置每个文件的描述以及 space 的详细信息。通过这份配置信息,我们也实现了 basketballplayer 和 basketballplayer_int_vid
两个 space 共享同一份数据。以后如果想添加新的测试数据集,只要增加一个类似 basketballplayer 的数据目录即可。config.yaml
的具体内容见repo。
除却常用的 pytest 和 nebula-python 库之外,目前的测试框架还用到了 pytest-bdd 和 pytest-xdist 等插件。此外,为了更好地统一添加测试用例 feature 文件的格式,我们引入了社区的reformat-gherkin工具,并基于此做了部分格式的调整,来保持与 openCypher TCK feature 文件的格式统一。
目前 nebula-python 和 reformat-gherkin 两款插件都是通过源码直接安装,我们在nebula-graph/tests
下提供了Makefile
来简化用户的操作流程。执行测试的所有环境准备只需要执行命令:
$ cd nebula-graph/tests && make init-all
我们也将上述格式检查集成到了 GitHub Action 的 CI 流程中,如果用户修改的测试文件格式不合预期,可通过make fmt
命令做本地的格式化处理。
由上篇所述,现在 nebula 的集成测试变为 “黑盒” 测试,用户不再需要关心自己编写的语句怎么调用,调用什么函数比较符合预期结果。只要按照约定的规范,使用近似 “自然语言” 的方式在 feature 文件中描述自己的用例即可。以下是一个测试用例的示例:
Feature: Variable length pattern match (m to n)
Scenario: both direction expand with properties
Given a graph with space named "basketballplayer"
When executing query:
"""
MATCH (:player{name:"Tim Duncan"})-[e:like*2..3{likeness: 90}]-(v)
RETURN e, v
"""
Then the result should be, in any order, with relax comparison:
| e | v |
| [[:like "Tim Duncan"<-"Manu Ginobili"], [:like "Manu Ginobili"<-"Tiago Splitter"]] | ("Tiago Splitter") |
Given
提供测试的初始条件,这里初始化一个名为 "basketballplayer" 的 space。When
描述测试的输入,即 nGQL 语句。Then
给出期望结果和期望比较的方式,这里表示无序宽松比较表格中的结果。
Feature 文件是 Gherkin 语言描述的一种文件格式,主要由如下几个部分构成:
每个 Scenario 又分为了不同的 step,每个 step 都有特殊的意义:
由上面描述可知,Scenario 就是有一个个的 step 组成,nebula 在兼容 openCypher TCK 的 step 基础上又定制了一些特有的步骤来方便测试用例的书写:
Given a graph with space named "basketballplayer"
:使用预先导入 “basketballplayer” 数据的 space;creating a new space with following options
:创建一个含有如下参数的新的 space,可以指定 name、partition_num、replica_factor、vid_type、charset 和 collate 等参数;load "basketballplayer" csv data to a new space
:向新 space 导入 “basketballplayer” 数据集;profiling query
:对 query 语句执行PROFILE
,返回结果中会含有 execution plan;wait 3 seconds
:等待 3 秒钟,在 schema 相关的操作时往往需要一定的数据同步时间,这时就可以用到该步骤;define some list variables
:定义一些变量表示元素很多的 List 类型,方便在期望结果中书写对应的 List;the result should be, in any order, with relax comparison
:执行结果进行无序宽松比较,意味着期望结果中用户写了什么就比较什么,没写的部分即使返回结果中有也不作比较;the result should contain
:返回结果必须包含期望结果;the execution plan should be
:比较返回结果中的执行计划。除却以上的这些步骤,还可根据需要定义更多的 steps 来加速测试用例的开发。
根据TCK 的描述可知,openCypher 定义了一组图语义的表示方式来表达期望的返回结果。这些点边的格式借鉴了MATCH
查询中的 pattern,所以如果熟悉 openCypher 的查询,基本可以很容易理解 TCK 测试场景中的结果。比如部分图语义的格式如下所示:
(:L {p:1, q:"string"})
;[:T {p:0, q:"string"}]
;<(:L)-[:T]->(:L2)>
。但是 Nebula Graph 同 Neo4J 的在图模型上还是有一些不同,比如在 Nebula Graph 中每个 Tag 均可以有自己的属性,那么按照现有的表述方式是不能描述含有多个带属性 Tag 的 vertex 的。在边的表示上也有差异,Nebula Graph 的 Edge Key 是由四元组组成<src, type, rank, dst>
,而现有的表示也不能描述边的 src、dst 和 rank 的值。故而在考虑了这些差异之后,我们扩充了现有 TCK 的 expected results 表达:
("VID" :T1{p:0} :T2{q: "string"})
;[:type "src"->"dst"@rank {p:0, q:"string"}]
;通过上述的点边描述方式上的扩充,即兼容 TCK 现有用例,又契合了 Nebula Graph 的设计。在解决了表达方式上的问题后,面临的下一个问题是如何高效无误地转化上述的表示到具体的数据结构,以便能够跟真正的查询结果做比较。在考虑了正则匹配、parser 解析等方案后,我们选择构造一个解析器的方式来处理这些具有特定语法规则的字符串,这样做的好处有如下的几点:
借助 ply.yacc 和 ply.lex 两个 library,我们可以用少量的代码实现上述复杂的需求,具体实现见nbv.py 文件。
目前的测试流程变为:
目前 Nebula Graph 所有的 feature 用例均位于 github.com/vesoft-inc/nebula-graph repo 中的tests/tck/features目录中。
$ cd /path/to/nebula-graph/tests
$ make up # 启动 nebula graph 服务
$ make fmt # 格式化
$ make tck # 执行 TCK 测试
$ mak
e down
当编写的用例需要调试时,便可以使用 pytest 支持的方式来进一步的调试,比如重新运行上次过程中失败的用例:
$ pytest --last-failed tck/ # 运行 tck 目录中上次执行失败的用例
$ pytest -k "match" tck/ # 执行含有 match 字段的用例
也可以在 feature 文件中对特定的一个 scenario 打上一个 mark,只运行该被 mark 的用例,比如:
# in feature file
@testmark
Scenario: both direction expand with properties
Given a graph with space named "basketballplayer"
...
# in nebula-graph/tests directory
$ pytest -m "testmark" tck/ # 运行带有 testmark 标记的测试用例
站在前人的肩膀之上才让我们找到更适合 Nebula Graph 的测试方案,在此也一并感谢文中提到的所有开源工具和项目。
在实践 pytest-bdd 的过程中,也发现其中一些不完美的地方,比如其跟 pytest-xdist 等插件兼容性的问题 (gherkin-reporter),还有 pytest 没有原生提供 global scope 级别的 fixture 等。但终究其带给 Nebula Graph 的收益要远大于这些困难。
上篇中有提到不需要用户进行编程,并非凭空想象,当我们把上述的模式固定后,可以开发一套添加测试用例的脚手架,让用户在页面上进行数据 “填空”,自动生成对应的 feature 测试文件,如此便可进一步地方便用户,此处可以留给感兴趣的社区用户来做些尝试了。