【實踐】Golang的單元測試入門go test

go test命令,相信大家都不陌生,常見的情況會使用這個命令做單測試、基準測試和http測試。go test還是有很多flag 可以幫助我們做更多的分析,比如測試覆蓋率,cpu分析,內存分析,也有很多第三方的庫支持test,cpu和內存分析輸出結果要配合pprof和go-torch來進行可視化顯示,可以看一下之前的這篇帖子 golang 使用pprof和go-torch做性能分析,這篇帖子總結一下go test的一些常用方式和推薦一些很棒的第三方庫。

go test文件命名是以_test.go為綴。例如userInfo_test.go。在github上寫了一個小的項目,包含常見的測試方法: https://github.com/lpxxn/gotest 。app1里是基本的測試方法。app2里包含了一些第三方的庫輔助我們更方便的測試。 大家可以下載來來 go test一下試試

測試函數以Test或者Bench為前綴開始,如:

func TestXXXXXXX(t *testing.T)
func BenchXXXXXX(b *testing.B)
func TestMain(m *testing.M)

看一下testing.T和testing.B者有組合 common

>type T struct {
    common
    isParallel bool context *testContext // For running tests and subtests.
}

type B struct {
    common
    importPath string // import path of the package containing the benchmark
    context          *benchContext
    N int previousN int           // number of iterations in the previous run
    previousDuration time.Duration // total duration of the previous run
    benchFunc        func(b *B)
    benchTime        time.Duration
    bytes            int64
    missingBytes bool // one of the subbenchmarks does not have bytes set.
    timerOn          bool showAllocResult bool result           BenchmarkResult
    parallelism int // RunParallel creates parallelism*GOMAXPROCS goroutines // The initial states of memStats.Mallocs and memStats.TotalAlloc.
 startAllocs uint64
    startBytes  uint64 // The net total of this test after being run.
 netAllocs uint64
    netBytes  uint64
}

common包含了T和B的所有公共方法,常見的比如Log()日志信息,Error() 錯誤信息,Fail()致命錯誤等方法,

TestMain(*testing.M)方法有些特殊,在一個包內只能有一個TestMain方法。這個方法會在測試方法運行前調用,相當于main()方法。我們可以在這個方法內做一些初始化數據操作等。看一下testing.M結構體

// M is a type passed to a TestMain function to run the actual tests.
type M struct {
    deps       testDeps
    tests      []InternalTest
    benchmarks []InternalBenchmark
    examples   []InternalExample

    timer *time.Timer
    afterOnce sync.Once

    numRun int }

專為TestMain準備

先以app1來對基本的test進行解說,app1的項目結構為。
 具體的代碼大家看一下就好,都是一些特別簡單的方法。

測試指定函數

簡單的測試方法

func TestNewUserInfo(t *testing.T) {
    u := NewUserInfo() if len(u.Name) == 0 {
        t.Error("name is empty")
    }
}

得到新創建的user信息,檢查name是否為空,如果為空則錯誤。

-run 后面的參數是正則,所有匹配這正則的方法都會被運行,比如測試所有包含user(不區分大小寫)的測試方法:

go test -v -run="(?i)user"

image

-v 是用于輸出所有Log的信息

也可以指寫具體的方法名,只要包含這些名稱的測試方法就會運行,如果要測試多個方法,名稱用"|"分開

go test -v -run=TestGetOrderList
go test -v -run="TestGetOrderList|TestNewUserInfo"

執行的結果不用我多說,運行是否通過,最后還有運行的時長,方法實在在簡單了,執行的太快只精確到2位,所以0.00。

測試指定文件

測試指定的_test.go文件,需要注意的是在同一個包下,需要把測試文件和源文件都寫出來:

go test -v user_test.go user.go

測試文件夾內所有的test文件

直接在某個目錄運行go test命令就會運行這個文件夾下所有的_test.go文件內的測試方法。

go test -v

如果文件夾里還包含文件夾,可以添加 "./..."來遞歸測試。

go test -v

BenchMark 測試

benchMark通過一系列的性能指標來對我們的方法進行測試,比如cpu,內存。循環測試次數據等。

基本的命令是

go test -bench="."

-bench 類似于-run 可以跟正則表達式來控制執行的方法

測試方法

func BenchmarkUserInfoList(b *testing.B) {
    b.StopTimer() // do something
 b.StartTimer() for i := 0; i < b.N; i++ { // pretend delay //time.Sleep(time.Millisecond * 500)
        userInfoList := UserInfoList() if len(userInfoList) != 10 {
            b.Error("userInfoList is empty")
        }
    }
}
返回的結果

benchmark方法名加當前測試cpu內核的數量,這個可以通過-cpu 來設置數量,10000是執行的次數,就是代碼中的b.N 171679 ns/op 每次操作的耗時。

可以通過flag benchtime來控制執行時長

-benchmem 用于顯示內存的分配情況


808 B/op 表示每一調用需要808個字節, 35 allocs/op表示每一次調用有35次內存分配

當然還有很多flag 大家可以通過下面的命令查看官方文檔

go help testflag

TestMain

一個包下面只能有一個TestMain方法。這個方法就和main()方法差不太多。他會在其他的Test方法之前執行,我們可以利用他做一些初始化數據操作,執行完后釋放資源等操作。

例如我在TestMain里把當前的環境變量設置為dev。 然后加載配置,當然都是模擬的。

func TestMain(m *testing.M) {
    os.Setenv(config.ENV_STR, config.ENV_DEV)
    config.LoadConfig()
    fmt.Printf("Env is %s\n", os.Getenv(config.ENV_STR))
    fmt.Printf("config info %#v\n", config.ConfigInfo) // do something... eg: init data // ...
 os.Exit(m.Run())
}

在執行所有的go test時會先執行TestMain

測試代碼覆蓋率

測試覆蓋率就是運行我們的測試方法所有跑過的代碼占全部代碼的比例,比如我們跑一下user_test.go的所有測試方法,然后看一下覆蓋率:

兩個命令:

go test -v -coverprofile cover.out user_test.go user.go
go tool cover -html=cover.out -o cover.html

一個是執行測試,另一個是把輸出的文件轉換成html

 用瀏覽器打開生成的html,綠顏色表示運行到的代碼,紅顏色表示沒有運行到的代碼,我的代碼是全部都運行到了。

測試http

先來一個原生的http handler方法

func HandleNewUser(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("name")
    fmt.Printf("url parameter user name is %s\n", name)

    say := r.FormValue("say")
    fmt.Printf("req say:' %s '\n", say)
    newUser := NewUserInfo()
    jData, _ := json.Marshal(newUser)
    w.WriteHeader(http.StatusOK)
    w.Header().Set("Content-Type", "application/json")
    w.Write(jData)
}

這個方法沒有什么邏輯,在url中獲取name參數,然后在post的form中獲取say參數,再返回一個user的json。

再看一下測試方法

func TestHandleNewUser(t *testing.T) {
    postBody := url.Values{}
    postBody.Add("say", "hello world")
    req := httptest.NewRequest(http.MethodPost, "http://localhost/createuser?name=linus", strings.NewReader(postBody.Encode()))
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    w := httptest.NewRecorder()
    HandleNewUser(w, req) if w.Code != http.StatusOK {
        t.Error("new user api error")
    } if w.Body.Len() == 0 {
        t.Error(" response is empty")
    }
    user := &model.UserInfo{}
    err := json.Unmarshal(w.Body.Bytes(), user) if err != nil {
        t.Error("response data error")
    }
    t.Logf("create user api response : %#v", user)
}

使用的是httptest包,先是創建了一個請求,url包含了name參數,body里有say參數。然后再通過NewRecorder創建一個responseWriter,把這兩個參數傳遞給我們的handler,再測試有沒有返回我們想要的執行結果。

如果你使用的web服務框架是gin。測試gin handler的代碼我寫在了app2里。有時間你可以看一下,大致的代碼:

func TestNewUserInfo(t *testing.T) {
    a := assert.New(t)

    router := gin.New() const path = "/newUserInfo" router.POST(path, NewUserInfo)

    body := url.Values{}
    body.Set("say", "hello world")
    rr, err := testutils.PostFormRequst(path + "?name=lp", router, body)
    a.Nil(err)

    user := &model.UserInfo{}
    err = json.Unmarshal(rr.Body.Bytes(), user)
    a.Nil(err)
    a.NotEqual(user.Name, "")
    a.NotEqual(user.Age, 0)
    t.Logf("%#v\n", user)
}

推薦幾個第三方的庫

有幾個我常用的第三方庫給大家推薦一下,相關的測試代碼都寫到 app2_thirdlib里了

github.com/stretchr/testify
github.com/jarcoal/httpmock

testify里有assert相信有其他語言基礎的同學一定知道他是做什么的,斷言處理比如

a.Nil(err)
a.NotEqual(user.Name, "")
a.NotEqual(user.Age, 0)

如果判斷的結果為false則測試失敗。

httpmock這個好玩,假如我們的項目有請求其他項目的api調用,但是我們沒有源碼,只知道返回結果。但是我們進行test測試時,要請求這個api。httpmock就是做這個用的,他們攔住我們的http請求,然后返回我們預置的response。

func TestUserRoleList(t *testing.T) {
    a := assert.New(t) // mock http
 testutils.HttpMockActivateNonDefault()
    httpmock.RegisterNoResponder(
        httpmock.NewStringResponder(http.StatusOK, fmt.Sprintf(`
        [
          { "id": 1, "name": "a" },
          { "id": 2, "name": "b" },
          { "id": 3, "name": "c" }
        ]
    `)))
    defer httpmock.DeactivateAndReset()

    router := gin.New() const path = "/userRoleList" router.GET(path, UserRoleList)
    rr, err := testutils.GetRequst(path, router)
    a.Nil(err)
    a.Equal(rr.Result().StatusCode, http.StatusOK)

    roles := make([]model.UserRole, 0)
    err = json.Unmarshal(rr.Body.Bytes(), &roles)
    a.Nil(err)
    a.NotEqual(len(roles), 0)
    t.Logf("len of roles: %d\n", len(roles))
}

我的UserRoleList方法調用了其他項目的api。httpmock會把這個http請求攔住,上面我們提前寫好的數組。

大家可以下載我的源碼,然后 go test ./... 一下試試。

作者:李鵬
出處:http://www.cnblogs.com/li-peng/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。

其他參考文檔:

(6)go test 測試用例那些事
https://cloud.tencent.com/developer/article/1376794
(7)GOLANG語言中文網 - Go語言標準庫 - 第九章 測試
https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter09/09.0.html
(8)golang 單元測試(gotests、mockery自動生成)
https://www.jishuwen.com/d/2vk4#tuit
(9)golang分層測試之http接口測試入門
https://studygolang.com/articles/16742

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容