动态

详情 返回 返回

burl: 一個簡單但靈活的 HTTP/3 測試框架,基於 bash 和 curl - 动态 详情

我創建了一個基於 bash 和 curl 的新 HTTP/3 測試框架:
https://github.com/kingluo/burl

背景

幾個月前,當我將 QUIC 補丁從 nginx 主線移植到 APISIX 並嘗試測試時,我發現test::nginx運行得不太好。它使用錯誤的監聽指令參數“http3”而不是“quic”(可能是由於版本差異)。

所以我想知道是否可以設計一個簡單的測試框架,因為最新的curl已經完全支持HTTP/3。

許多測試框架更喜歡 DSL(領域特定語言),例如 test::nginx 和Hurl,因為它更用户友好。而且它看起來更受數據驅動。然而,這對於希望擴展其功能的開發人員來説並不好,因為他們需要學習其背後的語言,花時間通過閲讀此測試框架的代碼庫來理解核心和擴展 API。換句話説,DSL 及其背後的語言為開發人員造成了知識鴻溝,因為它不直觀,無法將 DSL 對應的功能映射到語言本身。尤其是當框架本身能力不足,需要擴展時,這種差距會帶來巨大的學習成本。

以test::nignx為例,它是用Perl實現的。當我需要驗證響應正文時,通過預共享密鑰對其進行解密,內置函數沒有幫助,因為它只支持正則表達式匹配。所以我需要編寫一堆Perl 代碼來做到這一點。然而,我對 Perl 不熟悉,所以這不是一件容易的事。眾所周知,Perl 不是主流語言,學習曲線陡峭。當時我就想,如果我能用Python寫代碼該多好啊。

test::nginx的表達能力是有限的,儘管它已經支持使用curl發送請求,因為它不是一個腳本系統(當然,你可以使用Perl模塊擴展它,但由於框架設計的限制,這對於大多數人來説是一項艱鉅的任務)。

  1. 它假設您的輸入是文本字符串,您不能使用其他格式(例如 protobuf),也不能壓縮或加密文本。
  2. 唯一支持的輸出後處理是正則匹配,因此您無法解析和驗證其他形式的內容,例如解密。
  3. 您無法描述複雜的測試過程,例如發送三個請求來觸發請求集配額並驗證速率限制結果。
  4. 您不能發出其他類型的請求,例如 GRPC、SOAP。
    在我的實踐中,當我有這樣的需求超出test::nginx的能力時,我必須將它們委託給lua部分,這是非常麻煩的。

例如,當我需要解析並驗證CAS Auth插件的正確性時,涉及到很多客户端CAS協議細節,所以你必須在lua部分實現整個邏輯,這增加了維護的難度混合。眾所周知,Perl 和 Lua 都不是流行語言。;-(

您可以看到下面的測試用例涉及額外的 lua lib 開發來測試 CAS。並且,為了適應 test::nginx 缺乏響應解析,您必須在 lua 環境中進行轉換。相當煩人,至少對我來説,因為這些年來我寫了很多這樣的測試用例。

=== TEST 2: login and logout ok
--- config
    location /t {
        content_by_lua_block {
            local http = require "resty.http"
            local httpc = http.new()
            -- Test-specific CAS lua lib
            local kc = require "lib.keycloak_cas"

            local path = "/uri"
            local uri = "http://127.0.0.1:" .. ngx.var.server_port
            local username = "test"
            local password = "test"

            local res, err, cas_cookie, keycloak_cookie = kc.login_keycloak(uri .. path, username, password)
            if err or res.headers['Location'] ~= path then
                ngx.log(ngx.ERR, err)
                ngx.exit(500)
            end
            res, err = httpc:request_uri(uri .. res.headers['Location'], {
                method = "GET",
                headers = {
                    ["Cookie"] = cas_cookie
                }
            })
            assert(res.status == 200)
            ngx.say(res.body)

            res, err = kc.logout_keycloak(uri .. "/logout", cas_cookie, keycloak_cookie)
            assert(res.status == 200)
        }
    }
--- response_body_like
uri: /uri
cookie: .*
host: 127.0.0.1:1984
user-agent: .*
x-real-ip: 127.0.0.1

類似地,Hurl也使用 DSL 來描述測試。它的後端語言是 Rust。所以在可擴展性方面,它似乎比 test::nginx 差,因為它需要編譯代碼。然而,Rust 的生態系統比 Perl 的要好得多。所以你可以輕鬆地編寫代碼。但無論如何,您需要使用與 DSL 不同的語言來編寫代碼。

有沒有一個簡單的測試框架可以滿足以下需求?

  1. 沒有 DSL,只有簡單的 shell 腳本
  2. 易於擴展且獨立於編程語言
  3. 可用於初始化和測試任何服務器,不限於nginx,例如envoy

Bash 腳本

Shell是我們日常工作中終端下最重要的工具。每個管理員和開發人員都需要輸入命令來完成他們的工作。最有趣的是,shell也是一個編程平台,即腳本。當我們談論腳本時,我們關心兩個方面:shell 語言本身的強大功能和可用的命令,它們就像高級編程語言的標準庫。

在計算機歷史的遠古時代,shell的功能非常弱,命令也非常有限,於是Perl誕生了。但現在,情況發生了變化。許多高級的 shell 已經出現,例如 bash 和 zsh,它們為表達複雜邏輯提供了許多優秀的語言元素。還有越來越多的方便且優雅的命令,例如 jq,它用緊湊的表達式解析 JSON。在Linux和Mac中,默認的shell是bash,所以它就在我們眼皮底下,那麼為什麼不使用它來完成我們的測試任務呢?為什麼要費心去複雜的編程來完成工作,對吧?

Shell 命令看起來非常自然和直接。命令行是由單詞組成的,就像人類語言一樣,那麼為什麼要使用 DSL?此外,如果簡單的命令行不能滿足您的要求,您可以使用變量和流程控制語句對其進行重構,就像任何主流編程語言一樣。請注意,腳本編寫是可選的,即您不會遇到任何複雜性,因為簡單的命令行即可完成所有操作,因此一切都像 DSL 一樣工作,但效果更好。

例如,如果我需要發送 GET 並檢查 404,兩個簡單的命令就足夠了:

# send request
REQ /anything/foobar

# validate the response headers
HEADER -x "HTTP/3 404"

但我需要檢查 Prometheus 是否將 404 事件記錄為正確的數字,我該怎麼做?解析 Prometheus 指標消息並檢查 shell 函數中計數器的變化。一切看起來仍然很簡單,對吧?這就是腳本的美妙之處。

count_404() {
    curl http://127.0.0.1:9091/apisix/prometheus/metrics 2>&1 | \
        grep -F 'apisix_http_status{code="404",route="1",matched_uri="/anything/*"' | \
        awk '{print $2}'
}

# send the first request
REQ /anything/foobar

# get current counter
cnt1=`count_404`

# send the second request
REQ /anything/foobar

cnt2=`count_404`

# check if the counter is increased
((cnt2 == cnt1 + 1))

您可以使用流程控制語句來表達任何業務邏輯。

例如,要測試請求限制插件是否工作,可以使用循環。

# consume the quota
for ((i=0;i<2;i++)); do
    # send request
    REQ /httpbin/get -X GET --http3-only

    # validate the response headers
    HEADER -ix "HTTP/3 200"
done

# no quota
REQ /httpbin/get -X GET --http3-only
HEADER -x "HTTP/3 503"
HEADER -ix "x-ratelimit-remaining: 0"

burl: bash + curl

burl是一個基於bash和curl的簡單但靈活的HTTP/3測試框架。

設計

  1. 測試文件包含一個或多個測試用例,以及文件頭的可選初始部分,例如配置 nginx.conf 以及通過模板渲染啓動 nginx。
  2. 每個測試用例由三部分組成:

    1. 構造併發送請求,並將響應頭和響應正文保存到文件中以供後續步驟使用。
    2. 驗證響應標頭,例如使用“grep”。
    3. 解析並驗證響應正文,例如使用“jq”表達式。
  3. 易於擴展,您可以使用任何命令或其他高級腳本(例如 Python)驗證響應(步驟 2.1 和 2.2)。
  4. 任何命令失敗都會停止測試過程(通過“set -euo pipelinefail”bash 選項啓用)。
  5. 默認情況下會回顯測試過程(通過“set -x”bash 選項啓用)。

概要

#!/usr/bin/env burl

# Optional initialization here...
# Before all test cases are executed.
# For example, render nginx.conf and start nginx.
SET NGX_CONF_HTTP <<EOF
upstream test_backend {
    server $(dig +short nghttp2.org):443;

    keepalive 320;
    keepalive_requests 1000;
    keepalive_timeout 60s;
}
EOF

SET NGX_CONF <<'EOF'
location / {
    add_header Alt-Svc 'h3=":443"; ma=86400';
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_set_header Host "nghttp2.org";
    proxy_pass https://test_backend;
}
EOF

START_NGX

TEST 1: test case

# Send request
# REQ is a curl wrapper so you can apply any curl options to suit your needs.
# Check https://curl.se/docs/manpage.html for details.
REQ /httpbin/anything --http3 -d foo=bar -d hello=world

# Validate the response headers
# HEADER is a grep wrapper so you can apply any grep options and regular expressions to suit your needs.
HEADER -x "HTTP/3 200"

# Validate the response body, e.g. JSON body
# JQ is a jq wrapper so you can apply any jq options and jq expression to suit your needs.
JQ '.method=="POST"'
JQ '.form=={"foo":"bar","hello":"world"}'

TEST 2: another test case
# ...

# More test cases...

例子

APISIX

  1. 測試MTLS白名單
TEST 2: route-level mtls, skip mtls

ADMIN put /ssls/1 -d '{
    "cert": "'"$(<${BURL_ROOT}/examples/certs/server.crt)"'",
    "key": "'"$(<${BURL_ROOT}/examples/certs/server.key)"'",
    "snis": [
        "localhost"
    ],
    "client": {
        "ca": "'"$(<${BURL_ROOT}/examples/certs/ca.crt)"'",
        "depth": 10,
        "skip_mtls_uri_regex": [
            "/httpbin/get"
        ]
    }
}'

sleep 1

REQ /httpbin/get --http3-only

# validate the response headers
HEADER -x "HTTP/3 200"

# validate the response body, e.g. JSON body
JQ '.headers["X-Forwarded-Host"] == "localhost"'
  1. 測試 HTTP/3 Alt-Svc
ADMIN put /ssls/1 -d '{
    "cert": "'"$(<${BURL_ROOT}/examples/certs/server.crt)"'",
    "key": "'"$(<${BURL_ROOT}/examples/certs/server.key)"'",
    "snis": [
        "localhost"
    ]
}'

ADMIN put /routes/1 -s -d '{
    "uri": "/httpbin/*",
    "upstream": {
        "scheme": "https",
        "type": "roundrobin",
        "nodes": {
            "nghttp2.org": 1
        }
    }
}'



TEST 1: check if alt-svc works

altsvc_cache=$(mktemp)
GC "rm -f ${altsvc_cache}"

REQ /httpbin/get -k --alt-svc ${altsvc_cache}
HEADER -x "HTTP/1.1 200 OK"

REQ /httpbin/get -k --alt-svc ${altsvc_cache}
HEADER -x "HTTP/3 200"

SOAP

向 Web 服務發送 SOAP 請求並驗證響應。

使用表達式構造 JSON 輸入jo並驗證 JSON 輸出jq。

由 Python zeep.提供支持。

TEST 1: test a simple Web Service: Add two numbers: 1+2==3

SOAP_REQ \
    'https://ecs.syr.edu/faculty/fawcett/Handouts/cse775/code/calcWebService/Calc.asmx?WSDL' \
    Add `jo a=1 b=2` '.==3'

XML

Powered by xmltodict.

TEST 2: GET XML

# send request
REQ /httpbin/xml

# validate the response headers
HEADER -x "HTTP/1.1 200 OK"
HEADER -x "Content-Type: application/xml"

# validate the response XML body
XML '.slideshow["@author"]=="Yours Truly"'

結論

我認為HTTP測試框架並不難,不需要使用高級編程語言來實現。DSL實際上是框架實現和表示之間毫無意義的鴻溝,因為我們需要承擔翻譯成本,特別是對於編譯型編程語言,並且對於測試人員來説,學習它是一種負擔。那麼為什麼不統一它們並編寫腳本呢?藉助 bash(幾乎所有操作系統中日常使用的標準 shell)和curl,我們可以輕鬆處理所有測試工作。

歡迎大家討論並貢獻burl:

https://github.com/kingluo/burl

Add a new 评论

Some HTML is okay.