<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          xgo: 一款新鮮出爐的 Go 代碼測試?yán)?/h1>

          共 16829字,需瀏覽 34分鐘

           ·

          2024-05-23 08:00

          大家好,我是江湖十年。

          我曾經(jīng)寫過一篇文章《測試代碼終極解決方案 Monkey Patching》,里面介紹了 Go 語言中的猴子補(bǔ)丁方案。如今,時隔數(shù)月我又發(fā)現(xiàn)了一款新的工具可以實現(xiàn) Monkey Patching,本文將帶大家一起嘗鮮下這款新的測試工具表現(xiàn)如何。

          簡介

          簡單一句話介紹 xgo:它是一款強(qiáng)大的的 Go 測試工具集,功能包括 Trap、Mock、Trace、增量覆蓋率。

          當(dāng)然,開發(fā)中最常用的還是 Mock 功能,也是本文講解的重點(不要慌,其他功能也會介紹)。

          以下是 xgo 支持的所有平臺:


          x86 x86_64 (amd64) arm64 any other Arch...
          Linux Y Y Y Y
          Windows Y Y Y Y
          macOS Y Y Y Y
          any other OS... Y Y Y Y

          可以發(fā)現(xiàn),xgo 支持所有 go 語言支持的 OS 和 Arch,即它是跨平臺的。

          跨平臺這一點是最吸引我的地方,也是能讓 xgo 脫穎而出的關(guān)鍵。

          此外,xgo 還是并發(fā)安全的 Monkey Patching 方案,這點也是有別于其他方案的一個亮點。

          本文就以測試一個 HTTP 服務(wù)程序來演示 xgo 的基本使用。

          HTTP 服務(wù)程序示例

          假設(shè)我們有一個 HTTP 服務(wù)程序?qū)ν馓峁┯脩舴?wù),代碼如下:

          package main

          import (
           "encoding/json"
           "fmt"
           "io"
           "net/http"
           "strconv"

           "github.com/julienschmidt/httprouter"
           "gorm.io/driver/mysql"
           "gorm.io/gorm"
          )

          type User struct {
           ID   int
           Name string
          }

          func NewMySQLDB(host, port, user, pass, dbname string) (*gorm.DB, error) {
           dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
            user, pass, host, port, dbname)
           return gorm.Open(mysql.Open(dsn), &gorm.Config{})
          }

          func NewUserHandler(store *gorm.DB) *UserHandler {
           return &UserHandler{store: store}
          }

          type UserHandler struct {
           store *gorm.DB
          }

          func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
           w.Header().Set("Content-Type""application/json")

           body, err := io.ReadAll(r.Body)
           if err != nil {
            w.WriteHeader(http.StatusBadRequest)
            _, _ = fmt.Fprintf(w, `{"msg":"%s"}`, err.Error())
            return
           }
           defer func() { _ = r.Body.Close() }()

           u := User{}
           if err := json.Unmarshal(body, &u); err != nil {
            w.WriteHeader(http.StatusBadRequest)
            _, _ = fmt.Fprintf(w, `{"msg":"%s"}`, err.Error())
            return
           }

           if err := h.store.Create(&u).Error; err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            _, _ = fmt.Fprintf(w, `{"msg":"%s"}`, err.Error())
            return
           }
           w.WriteHeader(http.StatusCreated)
          }

          func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
           id := ps[0].Value
           uid, _ := strconv.Atoi(id)

           w.Header().Set("Content-Type""application/json")
           var u User
           if err := h.store.First(&u, uid).Error; err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            _, _ = fmt.Fprintf(w, `{"msg":"%s"}`, err.Error())
            return
           }
           _, _ = fmt.Fprintf(w, `{"id":%d,"name":"%s"}`, u.ID, u.Name)
          }

          func setupRouter(handler *UserHandler) *httprouter.Router {
           router := httprouter.New()
           router.POST("/users", handler.CreateUser)
           router.GET("/users/:id", handler.GetUser)
           return router
          }

          func main() {
           mysqlDB, _ := NewMySQLDB("localhost""3306""user""password""test")
           handler := NewUserHandler(mysqlDB)
           router := setupRouter(handler)
           _ = http.ListenAndServe(":8000", router)
          }

          這是一個簡單的 Web Server 程序,服務(wù)監(jiān)聽 8000 端口,提供了兩個接口:

          POST /users 用來創(chuàng)建用戶。

          GET /users/:id 用來查詢指定 ID 對應(yīng)的用戶信息。

          代碼邏輯比較簡單,我就不詳細(xì)講解了。

          為了保證業(yè)務(wù)的正確性,我們應(yīng)該對 (*UserHandler).CreateUser(*UserHandler).GetUser 這兩個 Handler 方法進(jìn)行單元測試。

          使用 xgo 進(jìn)行單元測試

          安裝

          xgo 使用前必須通過 go install 命令進(jìn)行安裝:

          $ go install github.com/xhd2015/xgo/cmd/xgo@latest
          $ xgo version
          1.0.35

          編寫測試代碼

          (*UserHandler).CreateUser 方法為例演習(xí)下如何編寫測試代碼。

          我們先來分析下這個方法的依賴項:

          首先UserHandler 這個結(jié)構(gòu)體本身有一個 store 屬性,依賴了 *gorm.DB 對象。

          其次,CreateUser 方法還接收三個參數(shù),它們都屬于 HTTP 網(wǎng)絡(luò)相關(guān)的外部依賴,你可以在我的另一篇文章《在 Go 語言單元測試中如何解決 HTTP 網(wǎng)絡(luò)依賴問題》中找到解決方案,就不在本文中進(jìn)行講解了。

          所以,我們應(yīng)該要想辦法解決 *gorm.DB 這個外部依賴。

          由于我們編寫代碼時,沒有為支持單元測試而專門使用接口來進(jìn)行解耦,導(dǎo)致 UserHandler 結(jié)構(gòu)體直接依賴了 *gorm.DB 結(jié)構(gòu)體對象,無法使用 gomock 工具對依賴項進(jìn)行 Mock。

          在不改變代碼的前提下,我們可以使用 xgo 提供的 Monkey Patching 技術(shù)為依賴對象 *gorm.DB 打上猴子補(bǔ)丁,以此來解決測試代碼中難以調(diào)用 h.store.First(&u, uid).Error 方法問題。

          要使用 xgo 編寫測試,需要引入 xgo 提供的 runtime 包,所以先使用 go get 命令將其添加到 go.mod 依賴項:

          go get github.com/xhd2015/xgo/runtime@latest

          使用 xgo(*UserHandler).CreateUser 方法編寫的測試代碼如下:

          package main

          import (
           "net/http/httptest"
           "strings"
           "testing"

           "github.com/stretchr/testify/assert"
           "github.com/xhd2015/xgo/runtime/mock"
           "gorm.io/gorm"
          )

          func TestUserHandler_CreateUser(t *testing.T) {
           mysqlDB := &gorm.DB{}
           handler := NewUserHandler(mysqlDB)
           router := setupRouter(handler)

           // 為 mysqlDB 打上猴子補(bǔ)丁,替換其 Create 方法
           mock.Patch(mysqlDB.Create, func(value interface{}) (tx *gorm.DB) {
            expected := &User{
             Name: "user1",
            }
            actual := value.(*User)
            assert.Equal(t, expected, actual)
            return mysqlDB
           })

           w := httptest.NewRecorder()
           req := httptest.NewRequest("POST""/users", strings.NewReader(`{"name": "user1"}`))
           router.ServeHTTP(w, req)

           // 斷言成功響應(yīng)
           assert.Equal(t, 201, w.Code)
           assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
           assert.Equal(t, "", w.Body.String())
          }

          我們使用 xgo 提供的 mock.Patch 方法,為 mysqlDB 對象的 Create 方法打了一個猴子補(bǔ)丁,然后使用匿名函數(shù)來實現(xiàn)這個 Create 方法,并且,在匿名函數(shù)的內(nèi)部還對 Create 方法接收到的參數(shù)進(jìn)行了驗證。

          沒錯,xgo 使用起來就是這么簡單,這也體現(xiàn)了猴子補(bǔ)丁的強(qiáng)大,它能原地修改 mysqlDB.Create 方法的實現(xiàn)。

          這樣,在執(zhí)行測試代碼時,測試方法將不再執(zhí)行 mysqlDB.Create 原方法內(nèi)部邏輯,而會被替換為調(diào)用在此定義的匿名函數(shù)邏輯。

          要執(zhí)行測試,我們不能像原來一樣使用 go test 來執(zhí)行測試函數(shù),需要將 go 命令替換為 xgo 命令:

          $ xgo test -v -run TestUserHandler_CreateUser          
          === RUN   TestUserHandler_CreateUser
          --- PASS: TestUserHandler_CreateUser (0.00s)
          PASS
          ok      github.com/jianghushinian/blog-go-example/test/xgo      0.524s

          測試通過。

          xgo 用法跟普通的 go test 用法完全相同,這也大大簡化了我們切換命令的心智負(fù)擔(dān),幾乎零成本切換。

          NOTE: 如果直接使用 go test -v -run TestUserHandler_CreateUser 執(zhí)行測試將得到報錯,讀者可自行測試。

          接下來我們再為 (*UserHandler).GetUser 方法編寫如下測試代碼:

          func TestUserHandler_GetUser(t *testing.T) {
           mysqlDB := &gorm.DB{}
           handler := NewUserHandler(mysqlDB)
           router := setupRouter(handler)

           // 為 mysqlDB 打上猴子補(bǔ)丁,替換其 First 方法
           mock.Patch(mysqlDB.First, func(dest interface{}, conds ...interface{}) (tx *gorm.DB) {
            assert.Equal(t, dest, &User{})
            assert.Equal(t, len(conds), 1)
            assert.Equal(t, conds[0], 1)

            u := dest.(*User)
            u.ID = 1
            u.Name = "user1"
            return mysqlDB
           })

           w := httptest.NewRecorder()
           req := httptest.NewRequest("GET""/users/1"nil)
           router.ServeHTTP(w, req)

           assert.Equal(t, 200, w.Code)
           assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
           assert.Equal(t, `{"id":1,"name":"user1"}`, w.Body.String())
          }

          與之前的套路如出一轍,使用 xgo 執(zhí)行測試:

          $ xgo test -v -run TestUserHandler_GetUser          
          === RUN   TestUserHandler_GetUser
          --- PASS: TestUserHandler_GetUser (0.00s)
          PASS
          ok      github.com/jianghushinian/blog-go-example/test/xgo      0.424s

          測試通過。

          現(xiàn)在,你也許會問,這種使用 mock.Patch 打過猴子補(bǔ)丁的測試代碼需要使用 xgo 才能執(zhí)行,那沒有用到 mock.Patch 的普通測試代碼能不能也用 xgo 執(zhí)行呢?答案是肯定的。

          比如我們隨意寫一個沒什么意義的 demo 測試:

          func TestDemo(t *testing.T) {
           t.Log("---------- TestDemo ----------")
          }

          使用 xgo 執(zhí)行測試代碼:

          $ xgo test -v -run TestDemo               
          === RUN   TestDemo
              main_test.go:65: ---------- TestDemo ----------
          --- PASS: TestDemo (0.00s)
          PASS
          ok      github.com/jianghushinian/blog-go-example/test/xgo      0.219s

          測試通過。

          使用 xgo 一次執(zhí)行全部測試代碼:

          $ xgo test -v               
          === RUN   TestUserHandler_CreateUser
          --- PASS: TestUserHandler_CreateUser (0.00s)
          === RUN   TestUserHandler_GetUser
          --- PASS: TestUserHandler_GetUser (0.00s)
          === RUN   TestDemo
              main_test.go:65: ---------- TestDemo ----------
          --- PASS: TestDemo (0.00s)
          PASS
          ok      github.com/jianghushinian/blog-go-example/test/xgo      0.175s

          測試通過。

          這樣我們就統(tǒng)一了執(zhí)行測試代碼的方式,所有測試都可以使用 xgo 來執(zhí)行。理論上,我們只需要將現(xiàn)有項目執(zhí)行 go test 的地方,替換成 xgo test 即可兼容所有測試代碼,這大大降低了引入 xgo 的遷移成本。

          xgo 其他功能

          前文提到,xgo 核心功能包括 Trap、Mock、Trace、增量覆蓋率。

          其實我們上面介紹的 mock.Patch 即為 Mock 功能,不過除了這個 API,xgo 還提供了另外一個 Mock API mock.Mock,實際上這兩個方法底層調(diào)用的是同一個函數(shù),用法也類似,我就不進(jìn)行演示了,感興趣的讀者可以深入源碼進(jìn)行研究。

          接下來我將依次介紹下 Trap、Trace、增量覆蓋率這幾個功能。

          Trap

          Trap 是 xgo 的核心,也是 Mock、Trace 功能的基礎(chǔ),它可以對 Go 函數(shù)進(jìn)行攔截。

          以下是一個官方文檔中使用 Trap 的例子:

          package main

          import (
           "context"
           "fmt"

           "github.com/xhd2015/xgo/runtime/core"
           "github.com/xhd2015/xgo/runtime/trap"
          )

          func init() {
           trap.AddInterceptor(&trap.Interceptor{
            Pre: func(ctx context.Context, f *core.FuncInfo, args core.Object, results core.Object) (interface{}, error) {
             if f.Name == "A" {
              fmt.Printf("trap A\n")
              return nilnil
             }
             if f.Name == "B" {
              fmt.Printf("abort B\n")
              return nil, trap.ErrAbort
             }
             return nilnil
            },
           })
          }

          func main() {
           A()
           B()
          }

          func A() {
           fmt.Printf("A\n")
          }

          func B() {
           fmt.Printf("B\n")
          }

          使用 go 命令執(zhí)行代碼:

          $ go run main.go
          A
          B

          代碼正常執(zhí)行。

          如果改為使用 xgo 執(zhí)行代碼;

          xgo run main.go
          trap A
          A
          abort B

          可以發(fā)現(xiàn),xgo 改變了代碼執(zhí)行結(jié)果,這就是 Trap 的強(qiáng)大之處,xgo 攔截了原有代碼的邏輯,進(jìn)而執(zhí)行攔截器內(nèi)部的邏輯。

          不過這種用法并不太多,我們更多的場景還是使用更上層的 Mock 功能來編寫測試代碼。

          Trace

          Trace 功能可以將 Go 程序執(zhí)行過程可視化,在一定程度上可以替代 Debug 工具,方便我們以可視化的方式進(jìn)行代碼調(diào)試。

          要想使用 Trace 功能,也很簡單,僅需要在使用 xgo 執(zhí)行測試代碼時加上 --strace 標(biāo)志:

          $ xgo test -v -run TestDemo --strace
          === RUN   TestDemo
              main_test.go:65: ---------- TestDemo ----------
          --- PASS: TestDemo (0.00s)
          PASS
          ok      github.com/jianghushinian/blog-go-example/test/xgo      0.162s

          執(zhí)行以上命令會在當(dāng)前目錄生成一個 TestDemo.json 文件,文件中即為可視化所需報告數(shù)據(jù)。

          接下來執(zhí)行如下命令即可開啟 Trace 可視化服務(wù):

          $ xgo tool trace TestDemo.json                     
          Server listen at http://localhost:7070

          此時會自動打開瀏覽器顯示類似如下頁面:

          Trace Demo

          左側(cè)列表可視化的展示了堆棧跟蹤信息,每項前面如果是藍(lán)色表示被調(diào)用函數(shù)正常返回,紅色表示返回錯誤。如果你使用 VSCode 開發(fā)代碼的話,點擊 VSCode 圖標(biāo)還會自動定位到 VSCode 中函數(shù)定義的位置,方便排查問題。

          遺憾的是,經(jīng)過筆者實測目前此功能還不夠穩(wěn)定,存在影響使用的 BUG,甚至經(jīng)常超時無法生成 Trace 文件。

          增量覆蓋率

          我們要介紹的最后一個功能是增量覆蓋率。

          go test 本身支持測試覆蓋率,不過 xgo 更近一步,它可以根據(jù) Git 變更,計算出增量測試覆蓋率,極大方便了代碼 review 的過程。

          為了查看變更代碼的增量覆蓋率,我們對 GetUser 方法代碼進(jìn)行了如下修改:

          git diff 命令輸出

          使用 xgo 命令輸出測試覆蓋率文件:

          $ xgo test -v -coverpkg . -coverprofile cover.out
          === RUN   TestUserHandler_CreateUser
          --- PASS: TestUserHandler_CreateUser (0.00s)
          === RUN   TestUserHandler_GetUser
          true...
          --- PASS: TestUserHandler_GetUser (0.00s)
          === RUN   TestDemo
              main_test.go:65: ---------- TestDemo ----------
          --- PASS: TestDemo (0.00s)
          PASS
          coverage: 54.8% of statements in .
          ok   github.com/jianghushinian/blog-go-example/test/xgo 0.962s

          NOTE: 由于此功能基于 Git,所以如果代碼不在 Git 倉庫,則執(zhí)行命令會報錯。并且筆者實測,如果一個 Git 倉庫存在多個項目情況下,執(zhí)行命令也會報錯。

          得到測試覆蓋率文件 cover.out 后,執(zhí)行以下命令啟動一個本地 Server 來展示測試覆蓋率:

          $ xgo tool coverage serve cover.out

          執(zhí)行命令后,xgo 會自動開啟瀏覽器并訪問 http://localhost:8000 地址:

          Incremental Coverage

          默認(rèn)展示的就是增量代碼測試覆蓋率。藍(lán)色表示已覆蓋,黃色表示未覆蓋,展示結(jié)果符合預(yù)期。

          我們也可以切換成查看全局代碼測試覆蓋率:

          Full Coverage

          以上就是 xgo 對增量測試覆蓋率的支持,還是能夠比較方便查看增量代碼測試覆蓋率的。

          總結(jié)

          xgo 作為一款 Monkey Patching 解決方案的工具,其支持 Trap、Mock、Trace、增量覆蓋率幾個功能,方便我們用來編寫單元測試。

          Trap 是 xgo 的核心,雖然不太常用,但上層的 Mock 和 Trace 都是基于 Trap 實現(xiàn)的。

          Mock 是我們用的最多的功能,其可以實現(xiàn)跨平臺的 Monkey Patching 解決方案。

          Trace 功能可以方便我們以可視化的形式對代碼進(jìn)行 Debug。

          而增量覆蓋率則可以方便我們在 review 代碼時可視化的感知到增量代碼的測試情況。

          總結(jié)下 xgo 目前的優(yōu)點和不足:

          優(yōu)點:

          • 相比于其他 Monkey Patching 解決方案,xgo 的跨平臺支持最好,這也是我認(rèn)為 xgo 最大的優(yōu)勢。
          • 并發(fā)安全,多個協(xié)程下 Mock 完全隔離。
          • 相比于 gomonkey 等,xgoPatch 后不需要進(jìn)行 Reset 操作對猴子補(bǔ)丁進(jìn)行恢復(fù)。
          • 使用方便,僅需要將 go 命令替換成 xgo 即可。

          不足:

          • 雖然使用時僅需將 go 命令替換成 xgo 即可,但這也意味著需要單獨安裝 xgo 才行,原來的 go test 命令執(zhí)行會報錯,不夠友好。
          • 不能隨意升級 Go 版本,至少目前筆者測試結(jié)果是這樣,我在使用 go1.22.0 時有些命令會報錯,換成 go1.21.0 則沒有問題。
          • Trace 功能速度比較慢,且存在 BUG。
          • 沒有提供 --help 命令,較為遺憾。

          以上優(yōu)點和不足,都是我個人基于當(dāng)前版本測試下來的主觀使用體驗,希望 xgo 能夠盡快發(fā)展起來,補(bǔ)齊短板。

          如果,你想了解 xgo 的誕生以及實現(xiàn)方案,前幾天 xgo 作者在 Go 夜讀分享了Go 夜讀第 151 期:xgo: 基于編譯期代碼重寫實現(xiàn) Mock 和 Trace: https://talkgo.org/t/topic/5514。

          本文完整代碼示例我放在了 GitHub 上,歡迎點擊查看。

          希望此文能對你有所幫助。

          P.S.

          本來預(yù)計這段時間不會再寫 Go 測試相關(guān)文章了,因為之前寫的測試相關(guān)文章已經(jīng)覆蓋了大部分日常編寫單元測試的場景。不過前段時間 xgo 作者聯(lián)系到我,跟我分享了 xgo 項目,解決了 gomonkey 項目的兼容性和并發(fā)問題。

          因為深入研究 Go 語言 Monkey Patching 解決方案這個方向的人很少,所以我對這個項目還是比較感興趣的,于是花時間體驗了下,便有了此文。

          xgo 項目給在 Go 語言中單元測試帶來了新的可能性,是目前我體驗過的最方便也是兼容性最好的 Monkey Patching 方案。

          誠然,xgo 項目還不夠成熟,它還非常年輕,剛開源出來不久,但是它開了個好頭,期待給它足夠的時間,能夠成長為 Go 社區(qū)里 Monkey Patch 解決方案中最為流行的項目之一。

          參考

          • xgo 源碼:https://github.com/xhd2015/xgo
          • xgo: 在go中使用-toolexec實現(xiàn)猴子補(bǔ)丁:https://blog.xhd2015.xyz/zh/posts/xgo-monkey-patching-in-go-using-toolexec/
          • xgo trace: 一個強(qiáng)大的Go堆??梢暬ぞ撸篽ttps://blog.xhd2015.xyz/zh/posts/xgo-trace_a-powerful-visualization-tool-in-go/
          • Go 夜讀第 151 期:xgo: 基于編譯期代碼重寫實現(xiàn) Mock 和 Trace:https://talkgo.org/t/topic/5514
          • 本文 GitHub 示例代碼:https://github.com/jianghushinian/blog-go-example/tree/main/test/xgo

          - END -



          推薦閱讀:

          Go 1.22 的新增功能系列之三:slices.Concat

          6 個必須嘗試的將代碼轉(zhuǎn)換為引人注目的圖表的工具

          Go 1.22 的新增功能系列之二:reflect.TypeFor

          Go早期是如何在Google內(nèi)部發(fā)展起來的

          2024 Gopher Meetup 武漢站活動

          go 中更加強(qiáng)大的 traces

          「GoCN酷Go推薦」我用go寫了魔獸世界登錄器?

          Go區(qū)不大,創(chuàng)造神話,科目三殺進(jìn)來了


          想要了解Go更多內(nèi)容,歡迎掃描下方??關(guān)注公眾號,掃描 [實戰(zhàn)群]二維碼  ,即可進(jìn)群和我們交流~



          - 掃碼即可加入實戰(zhàn)群 -


          分享、在看與點贊Go 

          瀏覽 359
          2點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報

          評論
          圖片
          表情
          推薦
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国V精品秘 久久久网 | 欧美日韩高清一区二区三区 | 操生在线视频 | 中文字幕永久免费视频 | 操她自拍 |