使用 go test 框架驅動的自動化測試
一. 背景
團隊的測試人員稀缺,無奈只能 “自己動手,豐衣足食”,針對我們開發的系統進行自動化測試,這樣既節省的人力,又提高了效率,還增強了對系統質量保證的信心。
我們的目標是讓自動化測試覆蓋三個環境,如下圖所示:
我們看到這三個環境分別是:
-
CI/CD 流水線上的自動化測試
-
發版後在各個 stage 環境中的自動化冒煙 / 驗收測試 [1]
-
發版後在生產環境的自動化冒煙 / 驗收測試
我們會建立統一的用例庫或針對不同環境建立不同用例庫,但這些都不重要,重要的是我們用什麼語言來編寫這些用例、用什麼工具來驅動這些用例。
下面看看方案的誕生過程。
二. 方案
最初組內童鞋使用了 YAML 文件 [2] 來描述測試用例,並用 Go 編寫了一個獨立的工具讀取這些用例並執行。這個工具運作起來也很正常。但這樣的方案存在一些問題:
- 編寫複雜
編寫一個最簡單的 connect 連接成功的用例,我們要配置近 80 行 yaml。一個稍微複雜的測試場景,則要 150 行左右的配置。
- 難於擴展
由於最初的 YAML 結構設計不足,缺少了擴展性,使得擴展用例時,只能重新建立一個用例文件。
- 表達能力不足
我們的系統是消息網關,有些用例會依賴一定的時序,但基於 YAML 編寫的用例無法清晰地表達出這種用例。
- 可維護性差
如果換一個人來編寫新用例或維護用例,這個人不僅要看明白一個個百十來行的用例描述,還要翻看一下驅動執行用例的工具,看看其執行邏輯。很難快速 cover 這個工具。
爲此我們想重新設計一個工具,測試開發人員可以利用該工具支持的外部 DSL 文法 [3] 來編寫用例,然後該工具讀取這些用例並執行。
注:根據 Martin Fowler 的《領域特定語言》[4] 一書對 DSL 的分類,DSL 有三種選型:通用配置文件 (xml, json, yaml, toml)、自定義領域語言,這兩個合起來稱爲外部 DSL。如:正則表達式、awk, sql、xml 等。利用通用編程語言片段 / 子集作爲 DSL 則稱爲內部 dsl,像 ruby 等。
後來基於待測試的場景數量和用例複雜度粗略評估了一下 DSL 文法 (甚至藉助 ChatGPT 生成過幾版 DSL 文法),發現這個“小語言” 那也是 “麻雀雖小五臟俱全”。如果用這樣的 DSL 編寫用例,和利用通用語言(比如 Python) 編寫的用例在代碼量級上估計也不相上下了。
既然如此,自己設計外部 DSL 意義也就不大了。還不如用 Python 來整。但轉念一想,既然用通用語言的子集了,團隊成員對 Python 又不甚熟悉,那爲啥不回到 Go 呢 ^_^。
讓我們進行一個大膽的設定:將 Go testing 框架作爲 “內部 DSL” 來編寫用例,用 go test 命令作爲執行這些用例的測試驅動工具。此外,有了 GPT-4 加持,生成 TestXxx、補充用例啥的應該也不是大問題。
下面我們來看看如何組織和編寫用例並使用 go test 驅動進行自動化測試。
三. 實現
1. 測試用例組織
我的《Go 語言精進之路 vol2》[5] 書中的第 41 條 “有層次地組織測試代碼”[6] 中對基於 go test 的測試用例組織做過系統的論述。結合 Go test 提供的 TestMain[7]、TestXxx 與 sub test[8],我們完全可以基於 go test 建立起一個層次清晰的測試用例結構。這裏就以一個對開源 mqtt broker 的自動化測試爲例來說明一下。
注:你可以在本地搭建一個單機版的開源 mqtt broker 服務作爲被測對象,比如使用 Eclipse 的 mosquitto[9]。
在組織用例之前,我先問了一下 ChatGPT 對一個 mqtt broker 測試都應該包含哪些方面的用例,ChatGPT 給了我一個簡單的表:
如果你對 MQTT 協議 [10] 有所瞭解,那麼你應該覺得 ChatGPT 給出的答案還是很不錯的。
這裏我們就以 connection、subscribe 和 publish 三個場景 (scenario) 來組織用例:
$tree -F .
.
├── Makefile
├── go.mod
├── go.sum
├── scenarios/
│ ├── connection/ // 場景:connection
│ │ ├── connect_test.go // test suites
│ │ └── scenario_test.go
│ ├── publish/ // 場景:publish
│ │ ├── publish_test.go // test suites
│ │ └── scenario_test.go
│ ├── scenarios.go // 場景中測試所需的一些公共函數
│ └── subscribe/ // 場景:subscribe
│ ├── scenario_test.go
│ └── subscribe_test.go // test suites
└── test_report.html // 生成的默認測試報告
簡單說明一下這個測試用例組織布局:
-
我們將測試用例分爲多個場景 (scenario),這裏包括 connection、subscribe 和 publish;
-
由於是由 go test 驅動,所以每個存放 test 源文件的目錄中都要遵循 Go 對 Test 的要求,比如:源文件以_test.go 結尾等。
-
每個場景目錄下存放着測試用例文件,一個場景可以有多個_test.go 文件。這裏設定_test.go 文件中的每個 TestXxx 爲一個 test suite,而 TestXxx 中再基於 subtest 編寫用例,這裏每個 subtest case 爲一個最小的 test case;
-
每個場景目錄下的 scenario_test.go,都是這個目錄下包的 TestMain 入口,主要是考慮爲所有包傳入統一的命令行標誌與參數值,同時你也針對該場景設置在 TestMain 中設置 setup 和 teardown。該文件的典型代碼如下:
// github.com/bigwhite/experiments/automated-testing/scenarios/subscribe/scenario_test.go
package subscribe
import (
"flag"
"log"
"os"
"testing"
mqtt "github.com/eclipse/paho.mqtt.golang"
)
var addr string
func init() {
flag.StringVar(&addr, "addr", "", "the broker address(ip:port)")
}
func TestMain(m *testing.M) {
flag.Parse()
// setup for this scenario
mqtt.ERROR = log.New(os.Stdout, "[ERROR] ", 0)
// run this scenario test
r := m.Run()
// teardown for this scenario
// tbd if teardown is needed
os.Exit(r)
}
接下來我們再來看看具體測試 case 的實現。
2. 測試用例實現
我們以稍複雜一些的 subscribe 場景的測試爲例,我們看一下 subscribe 目錄下的 subscribe_test.go 中的測試 suite 和 cases:
// github.com/bigwhite/experiments/automated-testing/scenarios/subscribe/subscribe_test.go
package subscribe
import (
scenarios "bigwhite/autotester/scenarios"
"testing"
)
func Test_Subscribe_S0001_SubscribeOK(t *testing.T) {
t.Parallel() // indicate the case can be ran in parallel mode
tests := []struct {
name string
topic string
qos byte
}{
{
name: "Case_001: Subscribe with QoS 0",
topic: "a/b/c",
qos: 0,
},
{
name: "Case_002: Subscribe with QoS 1",
topic: "a/b/c",
qos: 1,
},
{
name: "Case_003: Subscribe with QoS 2",
topic: "a/b/c",
qos: 2,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // indicate the case can be ran in parallel mode
client, testCaseTeardown, err := scenarios.TestCaseSetup(addr, nil)
if err != nil {
t.Errorf("want ok, got %v", err)
return
}
defer testCaseTeardown()
token := client.Subscribe(tt.topic, tt.qos, nil)
token.Wait()
// Check if subscription was successful
if token.Error() != nil {
t.Errorf("want ok, got %v", token.Error())
}
token = client.Unsubscribe(tt.topic)
token.Wait()
if token.Error() != nil {
t.Errorf("want ok, got %v", token.Error())
}
})
}
}
func Test_Subscribe_S0002_SubscribeFail(t *testing.T) {
}
這個測試文件中的測試用例與我們日常編寫單測並沒有什麼區別!有一些需要注意的地方是:
- Test 函數命名
這裏使用了 Test_Subscribe_S0001_SubscribeOK、Test_Subscribe_S0002_SubscribeFail 命名兩個 Test suite。命名格式爲:
Test_場景_suite編號_測試內容縮略
之所以這麼命名,一來是測試用例組織的需要,二來也是爲了後續在生成的 Test report 中區分不同用例使用。
- testcase 通過 subtest 呈現
每個 TestXxx 是一個 test suite,而基於表驅動的每個 sub test 則對應一個 test case。
- test suite 和 test case 都可單獨標識爲是否可並行執行
通過 testing.T 的 Parallel 方法可以標識某個 TestXxx 或 test case(subtest) 是否是可以並行執行的。
- 針對每個 test case,我們都調用 setup 和 teardown
這樣可以保證 test case 間都相互獨立,互不影響。
3. 測試執行與報告生成
設計完佈局,編寫完用例後,接下來就是執行這些用例。那麼怎麼執行這些用例呢?
前面說過,我們的方案是基於 go test 驅動的,我們的執行也要使用 go test。
在頂層目錄 automated-testing 下,執行如下命令:
$go test ./... -addr localhost:30083
go test 會遍歷執行 automated-testing 下面每個包的測試,在執行每個包的測試時會將 - addr 這個 flag 傳入。如果 localhost:30083 端口並沒有 mqtt broker 服務監聽,那麼上面的命令將輸出如下信息:
$go test ./... -addr localhost:30083
? bigwhite/autotester/scenarios [no test files]
[ERROR] [client] dial tcp [::1]:30083: connect: connection refused
[ERROR] [client] Failed to connect to a broker
--- FAIL: Test_Connection_S0001_ConnectOKWithoutAuth (0.00s)
connect_test.go:20: want ok, got network Error : dial tcp [::1]:30083: connect: connection refused
FAIL
FAIL bigwhite/autotester/scenarios/connection 0.015s
[ERROR] [client] dial tcp [::1]:30083: connect: connection refused
[ERROR] [client] Failed to connect to a broker
--- FAIL: Test_Publish_S0001_PublishOK (0.00s)
publish_test.go:11: want ok, got network Error : dial tcp [::1]:30083: connect: connection refused
FAIL
FAIL bigwhite/autotester/scenarios/publish 0.016s
[ERROR] [client] dial tcp [::1]:30083: connect: connection refused
[ERROR] [client] dial tcp [::1]:30083: connect: connection refused
[ERROR] [client] Failed to connect to a broker
[ERROR] [client] Failed to connect to a broker
[ERROR] [client] dial tcp [::1]:30083: connect: connection refused
[ERROR] [client] Failed to connect to a broker
--- FAIL: Test_Subscribe_S0001_SubscribeOK (0.00s)
--- FAIL: Test_Subscribe_S0001_SubscribeOK/Case_002:_Subscribe_with_QoS_1 (0.00s)
subscribe_test.go:39: want ok, got network Error : dial tcp [::1]:30083: connect: connection refused
--- FAIL: Test_Subscribe_S0001_SubscribeOK/Case_003:_Subscribe_with_QoS_2 (0.00s)
subscribe_test.go:39: want ok, got network Error : dial tcp [::1]:30083: connect: connection refused
--- FAIL: Test_Subscribe_S0001_SubscribeOK/Case_001:_Subscribe_with_QoS_0 (0.00s)
subscribe_test.go:39: want ok, got network Error : dial tcp [::1]:30083: connect: connection refused
FAIL
FAIL bigwhite/autotester/scenarios/subscribe 0.016s
FAIL
這也是一種測試失敗的情況。
在自動化測試時,我們一般會把錯誤或成功的信息保存到一個測試報告文件 (多是 html) 中,那麼我們如何基於上面的測試結果內容生成我們的測試報告文件呢?
首先 go test 支持將輸出結果以結構化的形式展現,即傳入 - json 這個 flag。這樣我們僅需基於這些 json 輸出將各個字段讀出並寫入 html 中即可。好在有現成的開源工具可以做到這點,那就是 go-test-report[11]。下面是通過命令行管道讓 go test 與 go-test-report 配合工作生成測試報告的命令行:
注:go-test-report 工具的安裝方法:go install github.com/vakenbolt/go-test-report@latest
$go test ./... -addr localhost:30083 -json|go-test-report
[go-test-report] finished in 1.375540542s
執行結束後,就會在當前目錄下生成一個 test_report.html 文件,使用瀏覽器打開該文件就能看到測試執行結果:
通過測試報告的輸出,我們可以很清楚看到哪些用例通過,哪些用例失敗了。並且通過 Test suite 的名字或 Test case 的名字可以快速定位是哪個 scenario 下的哪個 suite 的哪個 case 報的錯誤!我們也可以點擊某個 test suite 的名字,比如:Test_Connection_S0001_ConnectOKWithoutAuth,打開錯誤詳情查看錯誤對應的源文件與具體的行號:
爲了方便快速敲入上述命令,我們可以將其放入 Makefile 中方便輸入執行,即在頂層目錄下,執行 make 即可執行測試:
$make
go test ./... -addr localhost:30083 -json|go-test-report
[go-test-report] finished in 2.011443636s
如果要傳入自定義的 mqtt broker 的服務地址,可以用:
$make broker_addr=192.168.10.10:10083
四. 小結
在這篇文章中,我們介紹瞭如何實現基於 go test 驅動的自動化測試,介紹了這樣的測試的結構佈局、用例編寫方法、執行與報告生成等。
這個方案的不足是要求測試用例所在環境需要部署 go 與 go-test-report。
go test 支持將 test 編譯爲一個可執行文件,不過不支持將多個包的測試編譯爲一個可執行文件:
$go test -c ./...
cannot use -c flag with multiple packages
此外由於 go test 編譯出的可執行文件不支持將輸出內容轉換爲 JSON 格式 [12],因此也無法對接 go-test-report 將測試結果保存在文件中供後續查看。
本文涉及的源碼可以在這裏 [13] 下載 - https://github.com/bigwhite/experiments/tree/master/automated-testing
Gopher Daily(Gopher 每日新聞) 歸檔倉庫 - https://github.com/bigwhite/gopherdaily
我的聯繫方式:
-
微博 (暫不可用):https://weibo.com/bigwhite20xx
-
微博 2:https://weibo.com/u/6484441286
-
博客:tonybai.com
-
github: https://github.com/bigwhite
商務合作方式:撰稿、出書、培訓、在線課程、合夥創業、諮詢、廣告合作。
參考資料
[1]
驗收測試: http://en.wikipedia.org/wiki/Acceptance_testing
[2]
YAML 文件: https://tonybai.com/2019/02/25/introduction-to-yaml-creating-a-kubernetes-deployment/
[3]
外部 DSL 文法: https://tonybai.com/2022/05/10/introduction-of-implement-dsl-using-antlr-and-go
[4]
《領域特定語言》: https://book.douban.com/subject/21964984/
[5]
《Go 語言精進之路 vol2》: https://item.jd.com/13694000.html
[6]
第 41 條 “有層次地組織測試代碼”: https://book.douban.com/subject/35720729/
[7]
TestMain: https://pkg.go.dev/testing#Main
[8]
sub test: https://tonybai.com/2023/03/15/an-intro-of-go-subtest/
[9]
Eclipse 的 mosquitto: https://github.com/eclipse/mosquitto
[10]
MQTT 協議: https://mqtt.org/mqtt-specification/
[11]
go-test-report: https://github.com/vakenbolt/go-test-report
[12]
不支持將輸出內容轉換爲 JSON 格式: https://github.com/golang/go/issues/22996
[13]
這裏: https://github.com/bigwhite/experiments/tree/master/automated-testing
[14]
“Gopher 部落” 知識星球: https://wx.zsxq.com/dweb2/index/group/51284458844544
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/RKshV1hu61wabq4am906PA