go語言——測試

go語言測試

測試的目的是確認目標代碼在給定的場景下,有沒有按照指望工做 。一個場景是正向路經測試,就是在正常執行的狀況下,保證代碼不產生錯誤的測試。
另一些單元測試可能會測試負向路徑的場景,保證代碼不只會產生錯誤,並且是預期的錯誤。總之,無論如何調用或者執行代碼,所寫的代碼行爲都是可預期的 html

go語言爲咱們提供了測試框架testing和自帶go test命令來實現單元測試和性能測試。json

一、單元測試

go語言自帶的testing 框架使用:windows

  • 測試文件名必須以 _test.go結尾。 好比 main_test.go 或者XXX_test.go
  • 測試用例函數必須以Test開頭,首字母大寫。 TestXXX 好比TestAdd
  • 測試用例函數的參數必須是 *testing.T 好比 func TestAdd(t *testing.T)()
  • 一個測試文件中能夠有多個測試用例函數
  • go test 命令,若是運行正確,無日誌,錯誤時,會輸出日誌
  • go test -v 命令,運行正確或者錯誤,都會輸出日誌
  • go test -v xxx_test.go xxx.go 。。。測試單個測試文件 好比:go test -v main_test.go main.go b.go c.go ,須要制定測試方法所在的文件,有幾個寫幾個
  • 測試單個函數,go test -v -test.run 函數名

1.1 基礎單元測試

好比咱們有一個函數Sum(a,b int) int 須要測試,bash

main.go服務器

package main

func  Sum(a,b int )int{
    return  a+b
}

main_test.go網絡

package main

import (
    "testing"
)

func TestSum(t *testing.T){
    sum := Sum(1, 2)
    if sum==3{
        t.Logf("成功,結果爲%v",sum)
    }else{
        t.Fatalf("有錯誤,結果爲%v",sum)
    }
}

運行測試命令;併發

//命令行執行 go test 命令
PASS
ok      _/D_/gopath/src/a_tour_of_go/testing    1.301s
//執行go test -v 命令
=== RUN   TestSum
--- PASS: TestSum (0.00s)
    main_test.go:10: 成功,結果爲3
PASS
ok      _/D_/gopath/src/a_tour_of_go/testing    1.302s
//執行 go test -v main_test.go main.go
結果同上,由於咱們就寫了一個測試文件。
//執行 go test -v -test.run TestSum
結果同上,由於只有一個函數

其中,*testing.T 有一些方法方便使用app

type T
    func (c *T) Error(args ...interface{})
    func (c *T) Errorf(format string, args ...interface{})
    func (c *T) Fail()
    func (c *T) FailNow()
    func (c *T) Failed() bool
    func (c *T) Fatal(args ...interface{})
    func (c *T) Fatalf(format string, args ...interface{})
    func (c *T) Helper()
    func (c *T) Log(args ...interface{})
    func (c *T) Logf(format string, args ...interface{})
    func (c *T) Name() string
    func (t *T) Parallel()
    func (t *T) Run(name string, f func(t *T)) bool
    func (c *T) Skip(args ...interface{})
    func (c *T) SkipNow()
    func (c *T) Skipf(format string, args ...interface{})
    func (c *T) Skipped() bool

好比咱們經常使用,Fatalf() 出現錯誤是調用會打印,而且結束函數, Logf() 打印自定義信息。框架

1.2 表組測試

所謂的表組測試,基本上和單元測試同樣,只不過它有好幾個不一樣輸入以及輸出組成的一組單元測試。函數

簡單修改:無非就是多測幾回不一樣的值唄

package main

import (
    "testing"
)

func TestSum(t *testing.T){
    sum := Sum(1, 2)
    if sum==3{
        t.Logf("成功,結果爲%v",sum)
    }else{
        t.Fatalf("有錯誤,結果爲%v",sum)
    }
    sum1 := Sum(4, 4)
    if sum1==8{
        t.Logf("成功,結果爲%v",sum1)
    }else{
        t.Fatalf("有錯誤,結果爲%v",sum1)
    }
    sum2 := Sum(5, 5)
    if sum2==10{
        t.Logf("成功,結果爲%v",sum2)
    }else{
        t.Fatalf("有錯誤,結果爲%v",sum2)
    }
}
//結果
=== RUN   TestSum
--- PASS: TestSum (0.00s)
    main_test.go:10: 成功,結果爲3
    main_test.go:16: 成功,結果爲8
    main_test.go:22: 成功,結果爲10
PASS
ok      a_tour_of_go/testing    1.253s

1.3 模仿調用

當咱們測試須要網絡訪問時,咱們並無聯網,又不能時時開啓服務器,因此這時候模擬網絡訪問就有必要了。

針對模擬網絡訪問,標準庫了提供了一個httptest包,可讓咱們模擬http的網絡調用。

首先咱們建立一個處理HTTP請求的函數,並註冊路由

package common

import (
    "net/http"
    "encoding/json"
)

func Routes(){
    http.HandleFunc("/sendjson",SendJSON)
}

func SendJSON(rw http.ResponseWriter,r *http.Request){
    u := struct {
        Name string
    }{
        Name:"張三",
    }

    rw.Header().Set("Content-Type","application/json")
    rw.WriteHeader(http.StatusOK)
    json.NewEncoder(rw).Encode(u)
}

很是簡單,這裏是一個/sendjsonAPI,當咱們訪問這個API時,會返回一個JSON字符串。如今咱們對這個API服務進行測試,可是咱們又不能時時刻刻都啓動着服務,因此這裏就用到了外部終端對API的網絡訪問請求。

func init()  {
    common.Routes()
}

func TestSendJSON(t *testing.T){
    req,err:=http.NewRequest(http.MethodGet,"/sendjson",nil)
    if err!=nil {
        t.Fatal("建立Request失敗")
    }

    rw:=httptest.NewRecorder()
    http.DefaultServeMux.ServeHTTP(rw,req)

    log.Println("code:",rw.Code)

    log.Println("body:",rw.Body.String())
}

運行這個單元測試,就能夠看到咱們訪問/sendjsonAPI的結果裏,而且咱們沒有啓動任何HTTP服務就達到了目的。這個主要利用httptest.NewRecorder()建立一個http.ResponseWriter,模擬了真實服務端的響應,這種響應時經過調用http.DefaultServeMux.ServeHTTP方法觸發的。

還有一個模擬調用的方式,是真的在測試機上模擬一個服務器,而後進行調用測試。

func mockServer() *httptest.Server {
    //API調用處理函數
    sendJson := func(rw http.ResponseWriter, r *http.Request) {
        u := struct {
            Name string
        }{
            Name: "張三",
        }

        rw.Header().Set("Content-Type", "application/json")
        rw.WriteHeader(http.StatusOK)
        json.NewEncoder(rw).Encode(u)
    }
    //適配器轉換
    return httptest.NewServer(http.HandlerFunc(sendJson))
}

func TestSendJSON(t *testing.T) {
    //建立一個模擬的服務器
    server := mockServer()
    defer server.Close()
    //Get請求發往模擬服務器的地址
    resq, err := http.Get(server.URL)
    if err != nil {
        t.Fatal("建立Get失敗")
    }
    defer resq.Body.Close()

    log.Println("code:", resq.StatusCode)
    json, err := ioutil.ReadAll(resq.Body)
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("body:%s\n", json)
}

模擬服務器的建立使用的是httptest.NewServer函數,它接收一個http.Handler處理API請求的接口。 代碼示例中使用了Hander的適配器模式,http.HandlerFunc是一個函數類型,實現了http.Handler接口,這裏是強制類型轉換,不是函數的調用。

這個建立的模擬服務器,監聽的是本機IP127.0.0.1,端口是隨機的。接着咱們發送Get請求的時候,再也不發往/sendjson,而是模擬服務器的地址server.URL,剩下的就和訪問正常的URL同樣了,打印出結果便可。

1.4 測試覆蓋率

就其性質而言,測試不多是完整的 。再多測試也不能說明程序沒有bug,測試能夠加強咱們的信心,讓咱們的程序在一個放心的環境中正常運行。

由單元測試的代碼,觸發運行到的被測試代碼的代碼行數佔全部代碼行數的比例,被稱爲測試覆蓋率,代碼覆蓋率不必定徹底精準,可是能夠做爲參考,能夠幫咱們測量和咱們預計的覆蓋率之間的差距,go test工具,就爲咱們提供了這麼一個度量測試覆蓋率的能力。

簡單來講就是一個參數 - coverprofile

好比:

main.go

package main

import "fmt"

func Tag(tag int){
    switch tag {
    case 1:
        fmt.Println("Android")
    case 2:
        fmt.Println("Go")
    case 3:
        fmt.Println("Java")
    default:
        fmt.Println("C")

    }
}

main_test.go

package main

import (
    "testing"
)

func TestTag(t *testing.T) {
    Tag(1)
    Tag(2)

}

執行命令: go test -v -coverprofile=c.out 輸出結果:

=== RUN TestTag
Android
Go
--- PASS: TestTag (0.00s)
PASS
coverage: 60.0% of statements
ok a_tour_of_go/testing 1.309s

獲得測試覆蓋率爲60% , 咱們以前的c.out 是生成的測試報告,咱們能夠看到當前目錄下有一個c.out文件

咱們能夠生成 html 文件 go tool cover -html=c.out 會直接打開一個網頁,顯示咱們的代碼,經過顏色區分。 固然也能夠go tool cover -html=c.out -o=tag.html 在當前目錄生成一個名爲tag.html的文件,雙擊打開,同樣的。

在這裏插入圖片描述

標記爲綠色的代碼行已經被測試了;標記爲紅色的尚未測試到。咱們根據沒有測試到的代碼邏輯,完善個人單元測試代碼便可。

2.基準測試

2.1 進行基準測試

基準測試是一種測試代碼性能的方法。想要測試解決同一問題的不一樣方案的性能,以及查看哪一種解決方案的性能更好時,基準測試就會頗有用。

基準測試也能夠用來識別某段代碼的 CPU或者內存效率問題,而這段代碼的效率可能會嚴重影響整個應用程序的性能。許多開發人員會用基準測試來測試不一樣的併發模式,或者用基準測試來輔助配置工做池的數量,以保證能最大化系統的吞吐量。

main_test.go

package main

import (
    "fmt"
    "testing"
)

func BenchmarkSprintf(b *testing.B){
    num:=10
    b.ResetTimer()//重置計時器
    for i:=0;i<b.N;i++{
        fmt.Sprintf("%d",num)
    }
}

這是一個基準測試的例子,從中咱們能夠看出如下規則:

  1. 基準測試的代碼文件必須以_test.go結尾
  2. 基準測試的函數必須以Benchmark開頭,必須是可導出的
  3. 基準測試函數必須接受一個指向Benchmark類型的指針做爲惟一參數
  4. 基準測試函數不能有返回值
  5. b.ResetTimer是重置計時器,這樣能夠避免for循環以前的初始化代碼的干擾
  6. 最後的for循環很重要,被測試的代碼要放到循環裏
  7. b.N是基準測試框架提供的,表示循環的次數,由於須要反覆調用測試的代碼,才能夠評估性能

運行命令go test -v -run=none -bench=. 查看結果

goos: windows
goarch: amd64
BenchmarkSprintf-8      10000000               134 ns/op
PASS
ok      _/D_/gopath/src/a_tour_of_go/testing    2.797s

-bench :是進行基準測試參數, =. 表示全部的函數,若是要特定函數只須要後面跟函數名,好比 -bench=BenchmarkSprintf

-run=none :的做用是,運行一個none 不存在的單元測試,避免單元測試輸出干擾。由於運行基準測試的時候是默認運行咱們的單元測試的。咱們爲了查看方便,就運行一個不存在的單元測試過濾掉。

輸出結果表示:

看到函數後面的-8了嗎?這個表示運行時對應的GOMAXPROCS的值。接着的10000000 表示運行for循環的次數,也就是調用被測試代碼的次數,最後的134 ns/op表示每次須要話費134納秒。

以上是測試時間默認是1秒,也就是1秒的時間,調用1000萬次,每次調用花費134納秒。若是想讓測試運行的時間更長,能夠經過-benchtime指定,好比3秒。

go test -bench=. -benchtime=3s -run=none

goos: windows
goarch: amd64
BenchmarkSprintf-8      30000000               121 ns/op
PASS
ok      _/D_/gopath/src/a_tour_of_go/testing    5.731s

2.2 性能對比

上面那個基準測試的例子,實際上是一個int類型轉爲string類型的例子,標準庫裏還有幾種方法,咱們看下哪一種性能更加。

func BenchmarkSprintf(b *testing.B){
    num:=10
    b.ResetTimer()
    for i:=0;i<b.N;i++{
        fmt.Sprintf("%d",num)
    }
}

func BenchmarkFormat(b *testing.B){
    num:=int64(10)
    b.ResetTimer()
    for i:=0;i<b.N;i++{
        strconv.FormatInt(num,10)
    }
}

func BenchmarkItoa(b *testing.B){
    num:=10
    b.ResetTimer()
    for i:=0;i<b.N;i++{
        strconv.Itoa(num)
    }
}

運行基準測試,看看結果

運行命令: go test -bench=. -run=none          
goos: windows
goarch: amd64
BenchmarkSprintf-8      10000000               126 ns/op
BenchmarkFormat-8       300000000                3.85 ns/op
BenchmarkItoa-8         300000000                3.93 ns/op
PASS
ok      _/D_/gopath/src/a_tour_of_go/testing    5.893s

從結果上看strconv.FormatInt函數是最快的,其次是strconv.Itoa,而後是fmt.Sprintf最慢。第一個最慢,咱們能夠經過-benchmem找到根本緣由。

運行命令: go test -bench=. -benchmem -run=none
goos: windows
goarch: amd64
BenchmarkSprintf-8      10000000               132 ns/op              16 B/op          2 allocs/op
BenchmarkFormat-8       300000000                3.97 ns/op            0 B/op          0 allocs/op
BenchmarkItoa-8         300000000                4.25 ns/op            0 B/op          0 allocs/op
PASS
ok      _/D_/gopath/src/a_tour_of_go/testing    6.073s

-benchmem能夠提供每次操做分配內存的次數,以及每次操做分配的字節數。結果顯示,效率高的兩個每次操做進行0次內存分配,每次分配操做0個字節,多是這個太簡單了,因此沒來得及分配。慢的那個每次操做進行2次內存分配,每次16個字節。 因此效率高低的緣由一目瞭然了。

在代碼開發中,對於咱們要求性能的地方,編寫基準測試很是重要,這有助於咱們開發出性能更好的代碼。不過性能、可用性、複用性等也要有一個相對的取捨,不能爲了追求性能而過分優化。

相關文章
相關標籤/搜索