基于 BDD 理论的 Nebula 集成测试框架重构(下篇)

上篇文章中,我们介绍了 Nebula Graph 的集成测试的演进过程。本篇就介绍一下向测试集合中添加一个用例,并成功运行所有的测试用例的过程。

环境准备

在构建 2.0 测试框架之初,我们定制了部分工具类来帮助测试框架快速地启停一个单节点的 nebula 服务,其中有检查端口冲突、修改部分配置选项等功能。原来的执行流程如下:

  1. 通过 python 脚本启动 nebula 的服务;
  2. 调用pytest.main并发执行所有的测试用例;
  3. 停止 nebula 的服务。

其中的不便之处在于,当需要给 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 语句。这样做,存在如下问题:

  1. 测试数据集大的情况,INSERT 语句会变得冗长,client 执行超时;
  2. 不易拓展新的测试数据集,需要将现成的 csv 数据文件构造成对应的 nGQL 语句文件;
  3. 不能复用相同的数据集,比如希望同一份 csv 导入到不同 VID 类型的 space 中测试,需要构造不同的 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 文件结构

Feature 文件是 Gherkin 语言描述的一种文件格式,主要由如下几个部分构成:

  • Feature:可以添加当前文件的“Title”,也可以详细描述文件内容;
  • Background:后续 Scenario 共同使用的步骤;
  • Scenario:由一个个步骤描述每个测试用例的场景;
  • Examples:可以进一步将测试场景和测试数据进行分离,简化当前 Feature 文件中 Scenarios 的书写;

每个 Scenario 又分为了不同的 step,每个 step 都有特殊的意义:

  • Given: 设置当前测试场景的初始条件,上述 Background 中只能含有 Given 类型的 step;
  • When: 给定当前测试场景的输入;
  • Then: 描述做完 When 的 step 后期望得到的结果;
  • And: 可以紧跟上述 Given/When/Then 中任何一种类型的 step,进一步补充上述的 step 的动作;
  • Examples: 类似上述 Examples 描述,不过作用的范围限定在单个 Scenario 中,不影响同 Feature 文件中的其他 Scenario 测试。

Steps

由上面描述可知,Scenario 就是有一个个的 step 组成,nebula 在兼容 openCypher TCK 的 step 基础上又定制了一些特有的步骤来方便测试用例的书写:

  1. Given a graph with space named "basketballplayer":使用预先导入 “basketballplayer” 数据的 space;
  2. creating a new space with following options:创建一个含有如下参数的新的 space,可以指定 name、partition_num、replica_factor、vid_type、charset 和 collate 等参数;
  3. load "basketballplayer" csv data to a new space:向新 space 导入 “basketballplayer” 数据集;
  4. profiling query:对 query 语句执行PROFILE,返回结果中会含有 execution plan;
  5. wait 3 seconds:等待 3 秒钟,在 schema 相关的操作时往往需要一定的数据同步时间,这时就可以用到该步骤;
  6. define some list variables:定义一些变量表示元素很多的 List 类型,方便在期望结果中书写对应的 List;
  7. the result should be, in any order, with relax comparison:执行结果进行无序宽松比较,意味着期望结果中用户写了什么就比较什么,没写的部分即使返回结果中有也不作比较;
  8. the result should contain:返回结果必须包含期望结果;
  9. the execution plan should be:比较返回结果中的执行计划。

除却以上的这些步骤,还可根据需要定义更多的 steps 来加速测试用例的开发。

Parser

根据TCK 的描述可知,openCypher 定义了一组图语义的表示方式来表达期望的返回结果。这些点边的格式借鉴了MATCH查询中的 pattern,所以如果熟悉 openCypher 的查询,基本可以很容易理解 TCK 测试场景中的结果。比如部分图语义的格式如下所示:

  1. 点的描述:(:L {p:1, q:"string"});
  2. 边的描述:[:T {p:0, q:"string"}];
  3. 路径的描述:<(: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 表达:

  1. 点的描述支持带属性的多个 Tag:("VID" :T1{p:0} :T2{q: "string"})
  2. 边的描述支持 src、dst 和 rank 的表示:[:type "src"->"dst"@rank {p:0, q:"string"}]
  3. 路径就是循环上述点边的表示即可,同 TCK 。

通过上述的点边描述方式上的扩充,即兼容 TCK 现有用例,又契合了 Nebula Graph 的设计。在解决了表达方式上的问题后,面临的下一个问题是如何高效无误地转化上述的表示到具体的数据结构,以便能够跟真正的查询结果做比较。在考虑了正则匹配、parser 解析等方案后,我们选择构造一个解析器的方式来处理这些具有特定语法规则的字符串,这样做的好处有如下的几点:

  1. 可以根据具体的语法规则让解析出来的 AST 符合查询返回结果的数据结构,两者再进行比较时,便是具体结构中的具体字段的校验了;
  2. 避免处理复杂的正则匹配字符串,减少解析的错误;
  3. 可以支持其他字符串解析的需求,比如正则表达式、列表、集合等

借助ply.yacc 和 ply.lex 两个 library,我们可以用少量的代码实现上述复杂的需求,具体实现见nbv.py 文件

测试流程

目前的测试流程变为:

1) 编写 Feature 文件

目前 Nebula Graph 所有的 feature 用例均位于 GitHub - vesoft-inc/nebula-graph: A distributed, fast open-source graph database featuring horizontal scalability and high availability. This is an archived repo for v2.5 only, from 2.6.0 +, NebulaGraph switched back to https://github.com/vesoft-inc/nebula repo 中的tests/tck/features目录中。

2) 启动 nebula graph 服务

$ cd /path/to/nebula-graph/tests
$ make up # 启动 nebula graph 服务

3) 本地执行测试

$ make fmt # 格式化
$ make tck # 执行 TCK 测试

4)停止 nebula graph 服务

$ 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 测试文件,如此便可进一步地方便用户,此处可以留给感兴趣的社区用户来做些尝试了。

交流图数据库技术?加入 Nebula 交流群请先填写下你的 Nebula 名片,Nebula 小助手会拉你进群~~

2 个赞