本文首发于 Nebula Graph 公众号 NebulaGraphCommunity,Follow 看大厂图数据库技术实践。

上篇文章中,我们介绍了 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.com/vesoft-inc/nebula-graph 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 小助手会拉你进群~~

想要和其他大厂交流图数据库技术吗?NUC 2021 大会等你来交流:NUC 2021 报名传送门

基于 BDD 理论的 Nebula 集成测试框架重构(下篇)的更多相关文章

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

    本文首发于 Nebula Graph 公众号 NebulaGraphCommunity,Follow 看大厂图数据库技术实践. 测试框架的演进 截止目前为止,在 Nebula Graph 的开发过程中 ...

  2. 基于特定领域国土GIS应用框架设计及应用

              基于特定领域国土GIS应用框架 设计及应用              何仕国 2012年8月16日   摘要: 本文首先讲述了什么是框架和特定领域框架,以及与国土GIS 这个特定领 ...

  3. C++反射机制:可变参数模板实现C++反射(使用C++11的新特性--可变模版参数,只根据类的名字(字符串)创建类的实例。在Nebula高性能网络框架中大量应用)

    1. 概要   本文描述一个通过C++可变参数模板实现C++反射机制的方法.该方法非常实用,在Nebula高性能网络框架中大量应用,实现了非常强大的动态加载动态创建功能.Nebula框架在码云的仓库地 ...

  4. 基于CPU版本的Caffe推理框架

    最近一段时间,认真研究了一下caffe.但是,里面内容过多,集合了CPU版本和GPU版本的代码,导致阅读起来有些复杂.因此,特意对caffe代码进行了重构,搭建一个基于CPU版本的Caffe推理框架. ...

  5. 网络框架重构之路plain2.0(c++23 without module) 综述

    最近互联网行业一片哀叹,这是受到三年影响的后遗症,许多的公司也未能挺过寒冬,一些外资也开始撤出市场,因此许多的IT从业人员加入失业的行列,而且由于公司较少导致许多人求职进度缓慢,很不幸本人也是其中之一 ...

  6. 网络框架重构之路plain2.0(c++23 without module) 环境

    接下来本来就直接打算分享框架重构的具体环节,但重构的代码其实并没有完成太多,许多的实现细节在我心中还没有形成一个定型.由于最近回归岗位后,新的开发环境需要自己搭建,搭建的时间来说花了我整整一天的时间才 ...

  7. 转-基于NodeJS的14款Web框架

    基于NodeJS的14款Web框架 2014-10-16 23:28 作者: NodeJSNet 来源: 本站 浏览: 1,399 次阅读 我要评论暂无评论 字号: 大 中 小 摘要: 在几年的时间里 ...

  8. 第三篇 基于.net搭建热插拔式web框架(重造Controller)

    由于.net MVC 的controller 依赖于HttpContext,而我们在上一篇中的沙箱模式已经把一次http请求转换为反射调用,并且http上下文不支持跨域,所以我们要重造一个contro ...

  9. 第二篇 基于.net搭建热插拔式web框架(沙箱的构建)

    上周五写了一个实现原理篇,在评论中看到有朋友也遇到了我的问题,真的是有种他乡遇知己的感觉,整个系列我一定会坚持写完,并在最后把代码开源到git中.上一篇文章很多人看了以后,都表示不解,觉得不知道我到底 ...

  10. 基于.net搭建热插拔式web框架(实现原理)

    第一节:我们为什么需要一个热插拔式的web框架? 模块之间独立开发 假设我们要做一个后台管理系统,其中包括“用户活跃度”.“产品管理”."账单管理"等模块.每个模块中有自己的业务特 ...

随机推荐

  1. js赋值的两种方式

    第一种 let obj=[ { value:undefined } ] obj[0].value.value='zahngsan' obj[0].value.label='张三' 机智的小伙伴,可能已 ...

  2. 【学到了】golang的[]byte可以append string类型的数据

    上代码: func Test_use_string(t *testing.T){ arr := make([]byte,0, 100) arr = append(arr, "abcd&quo ...

  3. Unity的SpriteAtlas实践

    我的环境 Unity引擎版本:Unity2019.3.7f1 AssetBundles-Browser 于2021-1-14拉取,github上最后提交日期是2019-12-14,在本文简称:ABBr ...

  4. 报错ValueError: Can't find 'adapter_config.json'

    前言 在做组内2030项目时,我具体做的一个工作是对大模型进行LoRA微调,在整个过程中有许多坑,其中有些值得记录的问题,于是便产生了这篇博客. 问题 我在得到微调好的模型后,需要对模型进行性能测评. ...

  5. Eslint 的rules一些配置 (.eslintrc.js文件中的rules选项)

    rules: { // off=0, warn=1, error=2, 如果是数组, 第二项表示参数option // indent: [2, 2], // 控制缩进为2 eqeqeq: 1, // ...

  6. Spark源码修改环境搭建

    过程中存在问题: maven编译scala工程报错java.lang.NoClassDefFoundError: scala/reflect/internal/Trees,解决方案看maven编译 1 ...

  7. 可选可输入的input框

    <input type="text" list="note" autocomplete="off"> <datalist ...

  8. 基于客户真实使用场景的云剪辑Timeline问题解答与代码实操

    本文为阿里云智能媒体服务IMS「云端智能剪辑」实践指南第6期,从客户真实实践场景出发,分享一些Timeline小技巧(AI_TTS.主轨道.素材对齐),助力客户降低开发时间与成本. 欧叔|作者 故事的 ...

  9. NC16696 [NOIP2001]统计单词个数

    题目链接 题目 题目描述 给出一个长度不超过200的由小写英文字母组成的字母串(约定;该字串以每行20个字母的方式输入,且保证每行一定为20个).要求将此字母串分成k份( 1 < k ≤ 40 ...

  10. NVME(学习笔记_杂谈)

    NVME 协议中一些概念的理解: 1.Namespace :可以将Namespace 理解为一片Logic Block的区域,一个Controller可以支持多个Namespace,每个Namespa ...