助力開發者,全方位解讀 APISIX 測試案例

Apache_APISIX 發表於 2022-07-19

背景資訊

Apache APISIX 是 Apache 軟體基金會下的雲原生 API 閘道器,它兼具動態、實時、高效能等特點,提供了負載均衡、動態上游、灰度釋出(金絲雀釋出)、服務熔斷、身份認證、可觀測性等豐富的流量管理功能。你可以使用 APISIX 來處理傳統的南北向流量,也可以處理服務間的東西向流量。同時,它也支援作為 K8s Ingress Controller 來使用。

通常情況下,想要保證軟體正常執行,在軟體上線前我們一般會使用各種技術和方法,通過手動或自動的方式對軟體的功能進行檢查以確保其執行正常。該操作我們稱之為 QA(測試)。測試一般分為單元測試、E2E 測試以及混沌測試。

單元測試是用來檢查單一模組的正確性(比如檢查某個 RPC 的序列化/反序列化、資料加解密是否正常),但是該測試缺乏對系統的全域性視角。而 E2E 測試(即端到端測試),可以補足單元測試的不足,該測試將整個系統和外部依賴服務跑起來,通過真實的軟體呼叫方式檢查本系統與其他系統的整合情況;混沌測試則通過在系統各元件間製造突發情況,如 OOM Kill、網路中斷等,測試整個系統錯誤錯誤的容忍程度與能力。APISIX 的測試更加偏向於 E2E 測試,保證自身功能和與其他系統整合的正確性。

APISIX 測試案例簡介

APISIX 作為全球最活躍的 API 閘道器,其穩定性及服務的健壯性需要得到一定的保障,那麼如何避免 APISIX 中潛在的錯誤呢?這裡就需要通過測試用例來實現了。

測試指令碼並不僅僅是一個被測試機器執行的程式檔案,對於開發者來講,可以通過測試指令碼完成軟體所有功能的測試,包括不同配置、不同輸入引數等情況下程式的執行狀況。而對於使用者來說測試提供了某一個功能模組的具體使用示例,例如:程式可以接受的配置和輸入,想要得到怎樣的輸出結果。使用者在參考使用文件時遇到不懂的地方,完全可以參考現有的測試用例,尋找是否有類似的使用場景。

在 APISIX 專案中,通常使用 Github Action 執行 CI 測試,執行下圖展示的測試指令碼。許多 APISIX 的開發者在編寫測試用例時,會遇到各種各樣的問題。希望通過本文,可以減少你在編寫 APISIX 測試案例時出現的錯誤。

img

編寫測試案例

APISIX的測試用例基於 Test::NGINX 測試框架編寫,該測試框架是在 Perl 語言基礎上實現的測試環境,可以提供基於指令碼的自動化測試能力,為 APISIX 當前如此大規模的測試與質量保證工作提供了支援。當然你不會使用 Perl 也沒有關係,因為大部分場景中是不需要編寫 Perl 程式碼的,僅使用 TEST::NGINX 封裝的能力即可,如果有特殊需求可以結合 Lua 程式碼的方式進行增強。

APISIX 的測試用例均存放在 ./apisix/t 目錄下面,接下來將以新增 opa 外掛為例為你介紹如何編寫測試案例。

  1. 你需要建立一個 .t 結尾的測試檔案,例如 ./t/plugin/opa.t。如果你是在已有功能上新增特性,可以直接在對應的測試檔案中新增測試用例。並在檔案中新增固定格式的 ASF 2.0 協議。
  2. 該部分主要作用是給 opa.t 這個檔案中所有的測試用例自動新增 no_error_log,這樣就不需要在每個程式碼下新增 error_log 相關的程式碼了,你可以直接複製使用這段程式碼。通過這種方式,可以減少一些重複的程式碼。
add_block_preprocessor(sub {
    my ($block) = @_;

    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {
        $block->set_value("no_error_log", "[error]");
    }

    if (!defined $block->request) {
        $block->set_value("request", "GET /t");
    }
});

run_tests();

DATA
  1. 每個測試用例都有固定的開頭,一般格式如下。
=== TEST 1: sanity

=== 測試用例起始的固定語法結構,TEST` `1 則代表是本檔案的第一個測試用例。sanity 則為該測試的名稱。一般以測試用例的具體目的命名。

  1. 接下來就是測試用例的正文部分了。在 APISIX 中幾乎每個外掛都會定義一些引數和屬性,並且會預先定義 JSON schema,因此我們需要先檢查該外掛的輸入引數是否能夠正常地校驗,通過這種方式就可以檢查我們輸入的資料是否可以正確地被 JSON schema 的規則去校驗。
--- config
    location /t {
        content_by_lua_block {
            local test_cases = {
                {host = "http://127.0.0.1:8181", policy = "example/allow"},
                {host = "http://127.0.0.1:8181"},
                {host = 3233, policy = "example/allow"},
            }
            local plugin = require("apisix.plugins.opa")
            for _, case in ipairs(test_cases) do
                local ok, err = plugin.check_schema(case)
                ngx.say(ok and "done" or err)
            end
        }
    }
--- response_body
done
property "policy" is required
property "host" validation failed: wrong type: expected string, got number

通過閱讀 opa 外掛的原始碼,可以看到 opa 外掛要求 hostpolicy 必須同時存在,因此此處需要定義三個規則。

  • 輸入正確的引數,包括 hostpolicy。因此返回結果將是 done
  • 僅輸入 host 引數。不符合 hostpolicy 同時存在的要求,所以該測試預期返回結果將是 property "policy" is required
  • 輸入型別錯誤(整數)的 host 值。因為在原始碼中設定了 host 引數必須是字串型別,所以返回結果將是property "host" validation failed: wrong type: expected string, got number
--- config
    location /t {
        content_by_lua_block {
...
                ngx.say(ok and "done" or err)
            end
        }
    }
--- response_body
done
...

一般情況下,每個測試案例中需要使用 /t 的函式,比如說你需要呼叫 Lua 程式碼,定義 location,你可以使用 content_by_lua_block 的方式呼叫一些程式碼輔助測試,最後將響應資訊以 ngx.say 的方式列印出來,然後再通過 --- response_body 的方式檢查上面程式執行的是否正確。無需手動輸入 requesterror,因為我們已經通過指令碼自動新增了。

            local plugin = require("apisix.plugins.opa")
            for _, case in ipairs(test_cases) do
                local ok, err = plugin.check_schema(case)
                ngx.say(ok and "done" or err)

以上程式碼表示匯入 APISIX Plugin opa 外掛的模組,並且呼叫 plugin.check_schema 函式。然後通過 for 迴圈,以此呼叫引數,並且根據測試情況返回對應的結果。

  1. 接下來,我們需要配置一個測試時使用的環境。對於外掛來說,就是建立一個路由,然後把外掛關聯到該路由上。建立完成後,我們就可以通過傳送請求校驗外掛的內部邏輯實現是否正確。
=== TEST 2: setup route with plugin
--- config
    location /t {
        content_by_lua_block {
            local t = require("lib.test_admin").test
            local code, body = t('/apisix/admin/routes/1',
                 ngx.HTTP_PUT,
                 [[{
                        "plugins": {
                            "opa": {
                                "host": "http://127.0.0.1:8181",
                                "policy": "example"
                            }
                        },
                        "upstream": {
                            "nodes": {
                                "127.0.0.1:1980": 1
                            },
                            "type": "roundrobin"
                        },
                        "uris": ["/hello", "/test"]
                }]]
                )
            if code >= 300 then
                ngx.status = code
            end
            ngx.say(body)
        }
    }
--- response_body
passed

在以上示例中,使用了 lib_test_admin 匯入到 t 函式,建立一個 id1 的路由,然後使用 PUT 的方法,傳入這些資料。在該測試中,我們並沒有對資料格式進行檢驗,因為該測試用例只要保證使用 Admin API 可以正常建立路由就可以了,當然我們也需要對異常進行判斷,如果狀態碼大於或者等於 300 就會列印出具體的資訊。

在 APISIX 的測試案例中,你會發現很多測試中都包含了以下程式碼:

local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT)

以上程式碼表示匯入 lib.test_admin 模組並使用其中 test 函式封裝來傳送請求,它減少了我們呼叫 APISIX Admin API 等 HTTP 介面時的重複程式碼,只需簡單呼叫並檢查返回結果即可。

  1. 在第三個測試中,我們不需要再重複建立路由,因為在第二個測試中已經建立了。
=== TEST 3: hit route (with correct request)
--- request
GET /hello?test=1234&user=none
--- more_headers
test-header: only-for-test
--- response_body
hello world    

上述示例定義了 request。因為我們在上一個測試中定義了 /hello/`test 的路徑,所以我們可以通過 GET 的方法向 /`hello 傳送請求,並然後它會傳送一個 test=1234user=nonequery 引數。你也可以通過 more_headers 的方式新增響應頭,比如給傳送到 hello 的請求新增一個叫 test-header 的響應頭。

以上測試是為了測試新增正確的請求頭是否可以成功。你也可以在後續的測試示例中新增錯誤頭,並驗證結果。如果你需要使用 POST 請求,也可以把上述程式碼中的 GET /hello 修改為 POST /hello

在有些測試中,你可能需要建立多個上游或者路由,此時你就可以定義一個陣列,然後在陣列中定義這些對應的值,並通過 for 方式迴圈地呼叫 t 函式,然後讓它把這個東西正常地通過 put 的方式來呼叫 APISIX 的 Admin API 介面來正常建立路由或者上游。該方式也是測試比較常見的方式,稱為:Table driving test,是一種通過表的方式驅動測試的方法,該方法減少部分重複出現的程式碼,比如 opa2.t。詳細介紹,請點選閱讀原文參考APISIX測試案例快速入門視訊。

執行測試案例

測試案例需要原始碼安裝 APISIX,接下來將重點介紹如何執行測試案例及報錯檢查。

本地執行

通常情況下,你可以在本地使用以下指令來執行測試案例:

   PATH=/usr/local/openresty/nginx/sbin:/usr/bin PERL5LIB=.:$PERL5LIB FLUSH_ETCD=1 prove -Itest-nginx/lib -r t/admin
  • PATH 指定了 openresty/nginx 所在的目錄,可以避免部分環境配置錯誤時引起的衝突問題,如果環境中的 OpenResty 安裝在其他位置,則也可以通過這個命令進行指定。
  • PERL5LIB 指定了使用 Perl 匯入到本地。匯入本路徑中存在的以及部分通過環境變數附加的 PERL 庫。
  • FLUSH_ETCD 指定了在每個測試檔案執行完成後,則清空所有資料,它需要呼叫etcdctl函式,需要確保在 PATH 中可以找到 etcdctl 可執行檔案。
  • prove 呼叫測試程式開始執行測試。
  • -Itest-nginx/lib 表示匯入 Itest-nginx/lib 這個庫。
  • -r 表示自動尋找測試檔案。如果指定的是一個路徑,則會尋找這個路徑下所有的測試檔案。

以下為上述命令的正常執行結果。

  • t/admin 表示指定測試用例搜尋路徑,此處也可以指定到唯一一個 .t檔案上進行限定。
    img

如果測試失敗,則會出現以下資訊:

img

以上資訊則會告訴你具體是哪個測試檔案中的哪個測試用例執行失敗了。

Github 執行

一般情況在 Github 中提交程式碼時,輸出的結果和在本地測試相似。

首先選擇錯誤的執行工作流,主要的測試用例均在 build 系列 CI 中。

img

我們可以看到,在該示例中,416 行出現了報錯。通過錯誤資訊,我們可以得到在某個測試檔案中的某個測試用例出現錯誤,開發者定向檢視修正即可。需要注意的是,CI 中可能存在一些奇怪的報錯,它們可能是因為CI環境的臨時異常導致的,如果未修改過對應模組中的程式碼,可以忽略這些錯誤。

img

總結

本文主要為大家介紹了測試的相關流程,以及在 APISIX 測試案例的構成和如何進行測試案例的編寫,希望通過本文你可以對 APISIX 的測試案例有一個大致的認識。

本文中只提到了 APISIX 測試框架中的一些核心內容,未能覆蓋 TEST::NGINX 框架中的全部內容,實際上 TEST::NGINX 中還有很多強大的能力,我們可以通過 Test::Nginx::Socket 的文件瞭解更多用法。如果你想學習更多編寫測試案例的知識,可以檢視 APISIX 測試案例快速入門視訊。