藉助 DSL 來簡化 Loadgen 配置

infinilabs發表於2023-11-07

引言

上篇文章中,我們介紹瞭如何用 Loadgen 來簡化 HTTP API 的整合測試。在實際使用中會發現,編寫測試時最令人“頭疼”的部分是設計測試的輸入和校驗程式的輸出,而針對後者 Loadgen 提供了豐富的條件測試  [1] 來對響應進行斷言。

回顧上篇文章的示例:

# loadgen.ymlvariables:
  - name: id    type: sequencerunner:
  assert_error: true
  assert_invalid: truerequests:
  - request:
      method: PUT      url: $[[env.PIZZA_SERVER]]/test_create_document_$[[id]]
    assert:
      equals:
        _ctx.response.body_json.success: true
    register:
      - collection: _ctx.response.body_json.collection  - request:
      method: POST      url: $[[env.PIZZA_SERVER]]/$[[collection]]/_doc      body: '{"hello": "world"}'
    assert:
      equals:
        _ctx.response.body_json.result: created

上述配置中各請求的斷言只有一條,但如果我們的檢驗邏輯更加複雜,需要組合多重測試條件,比如我們想盡可能多的檢驗響應體中的欄位來提高測試的可靠性,那麼斷言的部分將會迅速膨脹,可讀性也會隨之下降。

例如,針對如下響應:

{
  "took": 17,
  "timed_out": false,
  "hits": {
    "total": {
      "value": 100,
      "relation": "eq"
    },
    "max_score": 1.0,
    "hits": [...]
  },
  "aggregations": {
    "vavg": {
      "value": 51.0
    },
    "vcount": {
      "value": 50
    },
    "vmax": {
      "value": 100
    },
    "vmin": {
      "value": 2
    },
    "vsum": {
      "value": 2550
    }
  }}

我們可能會寫出這樣的配置:

assert:
  and:
    - range:
        _ctx.response.body_json.took:
          lt: 50
    - equals:
        _ctx.response.status: 200
        _ctx.response.body_json.time_out: false
        _ctx.response.body_json.hits.total.value: 100
        _ctx.response.body_json.max_score: 1.0
        _ctx.response.body_json.aggregations.vavg.value: 51
        _ctx.response.body_json.aggregations.vcount.value: 50
        _ctx.response.body_json.aggregations.vmax.value: 100
        _ctx.response.body_json.aggregations.vmin.value: 2
        _ctx.response.body_json.aggregations.vsum.value: 2550
    - regexp:
        _ctx.response.body_json.hits.total.relation: eq|gt|ge

不難發現,上面的配置看起來與原始的響應結構有很大的差別,寫起來十分繁瑣,看起來也不直觀,為了解決這一問題,我們為 Loadgen 的 YAML 配置設計了一種 DSL  [2]

更直觀的斷言配置

Loadgen DSL 針對各種斷言進行了著重的簡化,比如上述配置我們可以改寫成:

{
  took: <50,
  time_out: false,
  hits: {
    total: {
      value: 100,
      relation: /eq|gt|ge/,
    },
  },
  max_score: 1.0,
  aggregations: {
    vavg.value: 51,
    vcount.value: 50,
    vmax.value: 100,
    vmin.value: 2,
    vsum.value: 2550,
  },
}

這樣是不是“清爽”了許多?而且,有沒有發現這個語法和 JSON 很像?沒錯,Loadgen DSL  完全相容 JSON 語法!也就是說,可以直接把響應體複製下來,然後在其基礎上進行修改:

{
  // 用 <50 來測試此欄位的值是否小於 50
  "took": <50,
  // 普通的值將被測試欄位實際值是否與它相等
  "timed_out": false,
  "hits": {
    "total": {
      "value": 100,
      // 用正規表示式來檢查此欄位的值
      "relation": /eq|gt|ge/
    },
    "max_score": 1.0
  },
  "aggregations": {
    "vavg": {
      "value": 51.0
    },
    "vcount": {
      "value": 50
    },
    "vmax": {
      "value": 100
    },
    "vmin": {
      "value": 2
    },
    "vsum": {
      "value": 2550
    }
  }
}

注意到 Loadgen DSL 中欄位的引號是可以省略的,同時它也支援  notand 和  or 的邏輯組合:

{
  took: >0 and <50,
  relation: "eq" or "gt" or "ge",
  hits.total.value: not 0,
}

而對於較複雜的條件測試,比如  prefix/contains/in 等,可以透過函式呼叫語法來實現:

{
  blog.title: prefix("INFINI"),
  blog.tag: contains("Loadgen"),
  blog.status: in(["reviewed", "archived"]),
}

進一步的簡化

以上我們展示了 Loadgen DSL 是如何簡化斷言配置的,回想到上一章開頭所說的,HTTP API 測試中主要就是不斷髮起請求然後校驗其響應,那我們何不在 DSL 中將請求一起解析了呢?

於是,在現有的語法上稍作改進,我們便可以透過如下示例:

PUT $[[env.PIZZA_SERVER]]/test_create_document_$[[id]]
# // 注意這裡,因為我們定義 register 變數,因此需要使用完整語法
# register: [
#  {collection: "_ctx.response.body_json.collection"},
# ],
# assert: {
#   _ctx.response.body_json.success: true,
# },
POST $[[env.PIZZA_SERVER]]/$[[collection]]/_doc
{"hello": "world"}
# {result: "created"}

來替換掉本文最開頭示例中的  requests 部分。

上述示例提到了“完整語法”,在 Loadgen DSL 中,如下“簡短語法”的配置:

POST $[[env.PIZZA_SERVER]]/$[[collection]]/_doc
{"hello": "world"}
# 200 // 狀態碼是可選的
# {result: "created"}

等同於:

POST $[[env.PIZZA_SERVER]]/$[[collection]]/_doc
{"hello": "world"}
# assert: {
#   _ctx.response.status: 200,
#   _ctx.response.body_json: {result: "created"},
# }

Tips:

對於  assert 欄位,也可以透過元組表示式來使用簡短語法:

POST $[[env.PIZZA_SERVER]]/$[[collection]]/_doc
{"hello": "world"}
# assert: (200, {result: "created"})

對於 Loadgen DSL 詳細的語法定義可以參考本文的 附錄部分

最後一些最佳化

到目前為止,Loadgen DSL 幾乎可以替換掉 Loadgen YAML 配置的大部分內容,除了  variables 和  runner 這樣的全域性配置項。觀察一下現有的寫法,每個請求都是  METHOD URL 然後緊跟可選的請求體與斷言註釋,其實我們可以在 DSL 的最前面定義一些全域性的選項:

# variables: [
#   {name: "id", type: "sequence"},
# ],
# runner: {
#   assert_error: true,
#   assert_invalid: true,
# },
PUT $[[env.PIZZA_SERVER]]/test_create_document_$[[id]]
# register: [{
#   collection: "_ctx.response.body_json.collection",
# }],
# assert: {
#   _ctx.response.body_json: {success: true},
# },
POST $[[env.PIZZA_SERVER]]/$[[collection]]/_doc
{"hello": "world"}
# {result: "created"}

上述示例就等價於本文最開頭給出的配置。

附錄:Loadgen DSL 語法定義

以下是 Loadgen DSL 的 BNF [3] 語法定義:

grammer    ::= brief | full
brief      ::= status? object EOF
full       ::= fields EOF
status     ::= integer
expr       ::= expr1 (infixop expr1)*expr1      ::= literal             | array             | object             | funcall             | prefixop expr1             | '(' exprlist ')'exprlist   ::= (expr (',' expr)* ','?)?object     ::= '{' fields '}'fields     ::= (pair (',' pair)* ','?)?pair       ::= path ':' expr
path       ::= key ('.' key)*key        ::= name | string | integer
array      ::= '[' exprlist ']'funcall    ::= name '(' exprlist ')'literal    ::= null             | boolean             | integer             | float             | regex             | string
ignore     ::= whitespace              | comment
              /* ws: definition */<?TOKENS?>comment    ::= '//' char*name       ::= ident - keyword
keyword    ::= 'null'
             | 'true'
             | 'false'
             | 'not'
             | 'and'
             | 'or'ident      ::= id_start (id_start | '-' | digit)*id_start   ::= [_a-zA-Z]prefixop   ::= '-'
             | '>'
             | '<'
             | '>='
             | '<='
             | '=='
             | 'not'infixop    ::= 'and' | 'or'null       ::= 'null'boolean    ::= 'true' | 'false'integer    ::= digit+exponent   ::= ('e' | 'E') ('+' | '-')? integer
float      ::= integer exponent             | integer '.' integer exponent?digit      ::= [0-9]regex      ::= '/' ('\/' | char - '/')+ '/'string     ::= '"' (escape | char - '"')* '"'
             | "'" (escape | char - "'")* "'"escape     ::= '\b'
             | '\f'
             | '\n'
             | '\r'
             | '\t'
             | "\'"
             | '\"'
             | '\\'
             | '\/'char       ::= #x9             | [#x20-#xD7FF]
             | [#xE000-#xFFFD]
             | [#x10000-#x10FFFF]whitespace ::= [#x9#xA#xD#x20]+EOF        ::= $



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70029458/viewspace-2993357/,如需轉載,請註明出處,否則將追究法律責任。

相關文章