基於 BDD 理論的 Nebula 整合測試框架重構(下篇)

HarrisChu發表於2021-06-30

基於 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.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 測試檔案,如此便可進一步地方便使用者,此處可以留給感興趣的社群使用者來做些嘗試了。

相關文章