我創建了一個基於 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模塊擴展它,但由於框架設計的限制,這對於大多數人來説是一項艱鉅的任務)。
- 它假設您的輸入是文本字符串,您不能使用其他格式(例如 protobuf),也不能壓縮或加密文本。
- 唯一支持的輸出後處理是正則匹配,因此您無法解析和驗證其他形式的內容,例如解密。
- 您無法描述複雜的測試過程,例如發送三個請求來觸發請求集配額並驗證速率限制結果。
- 您不能發出其他類型的請求,例如 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 不同的語言來編寫代碼。
有沒有一個簡單的測試框架可以滿足以下需求?
- 沒有 DSL,只有簡單的 shell 腳本
- 易於擴展且獨立於編程語言
- 可用於初始化和測試任何服務器,不限於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測試框架。
設計
- 測試文件包含一個或多個測試用例,以及文件頭的可選初始部分,例如配置 nginx.conf 以及通過模板渲染啓動 nginx。
-
每個測試用例由三部分組成:
- 構造併發送請求,並將響應頭和響應正文保存到文件中以供後續步驟使用。
- 驗證響應標頭,例如使用“grep”。
- 解析並驗證響應正文,例如使用“jq”表達式。
- 易於擴展,您可以使用任何命令或其他高級腳本(例如 Python)驗證響應(步驟 2.1 和 2.2)。
- 任何命令失敗都會停止測試過程(通過“set -euo pipelinefail”bash 選項啓用)。
- 默認情況下會回顯測試過程(通過“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
- 測試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"'
- 測試 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