1. 程式人生 > >Go36-23,24,25-單元測試

Go36-23,24,25-單元測試

mil 清理臨時文件 pty float orf 重復執行 機器 所在 個性

單元測試

對於程序或軟件的測試分很多種,比如:單元測試、API測試、集成測試、灰度測試等。這裏主要針對單元測試進行講解。
Go 語言是一門很重視程序測試的語言,它不但自帶了testing包,還有專門用於程序測試的命令go test。要想真正用好一個工具,就需要了解它的核心邏輯。

測試源碼文件

單元測試,又稱程序員測試。就是程序員程序員們本該做的自我檢查的工作之一。
我們可以為Go程序編寫3類測試:

  • 功能測試(test)
  • 基準測試(benchmark),也稱性能測試
  • 示例測試(example)

前兩類測試,從名稱上就應該可以猜到它們的用途。示例測試,嚴格來講也是一種功能測試,只不過它更關註程序打印出來的內容。

一般情況下,一個測試源碼文件只會針對於某個命令源碼文件,或庫源碼文件做測試,所以一般並且也應該把他們放在同一個代碼包內。
測試源碼文件的主名稱應該以被測試源碼文件的主名稱為簽到,並且必須以“_test”為後綴。
每個測試源碼文件都必須至少包含一個測試函數。並且,從語法上講,每個測試源碼文件中,都可以包含用來做任何一類測試的測試函數,就是說把上面說的3類測試函數都塞進去也沒問題,只要把控好測試函數的分組和數量就可以了。
我們可以依據這些測試函數針對的不同程序實體,把他們分成不同額邏輯組。並且利用註釋以及幫助類的變量或函數來做分割。同時,還可以依據被被測試的文件中程序實體的先後順序來安排測試源碼文件中測試函數的順序。

測試函數的名稱和簽名

對於測試函數的名稱和簽名,Go語言也有明文的規定:

  • 功能測試函數,名稱必須以Test為前綴,並且只有一個*testing.T類型的參數。
  • 性能測試函數,名稱必須以Benchmark為前綴,並且只有一個*testing.B類型的參數。
  • 示例測試函數,名稱必須以Example為前綴,對參數沒有強制規定。

go test的主要流程

前面兩小節的內容,其實是go test命令的基本規則。只有測試源碼文件的名稱對了,測試函數的名稱和簽名也對了,在運行go test命令的時候,其中的測試代碼才會被運行。接著講主要流程。
當go test命令在開始運行時,會先做一下準備工作,比如:確定內部需要用到的命令,檢查指定的代碼包或源碼文件的有效性,以及判斷我們給予的標記是否合法,等等。

在準備工作完成後,go test命令就會針對每個被測試代碼包,依次的進行構建、執行包中符合要求的測試函數,清理臨時文件,打印測試結果。這就是通常情況下的主要測試流程。
這裏的“依次”二字,對於每個被測試代碼包,go test命令會串行地執行測試流程中的每個步驟。但是為了加快測試速度,通常會並發的對多個被測試代碼包進行功能測試。但是在最後打印測試結果的時候,會依照給定的順序逐個打印。
另一個方面,由於並發的測試會讓性能測試的結果存在偏差,所以性能測試一般都是串行進行的。具體說就是在所有構建步驟都做完之後,go test命令才會真正地開始進行性能測試。並且下個一個代碼包的性能測試的進行,會等到上一個代碼包的性能測試的結果打印完成才會開始,並且性能測試函數的執行也都會是串行的。

功能測試

先給出示例,然後主要講測試規則和流程

功能測試示例

這裏把示例單獨給出,首先是命令源碼文件:

// demo.go
package main

import (
    "errors"
    "flag"
    "fmt"
)

var name string

func init() {
    flag.StringVar(&name, "name", "bobody", "Your Name:")
}

func main() {
    flag.Parse()
    greeting, err := hello(name)
    if err != nil {
        fmt.Printf("ERROR: %s\n", err)
        return
    }
    fmt.Println(greeting, introduce())
}

func hello(name string) (string, error) {
    if name == "" {
        return "", errors.New("empyt name")
    }
    return fmt.Sprintf("Hello %s.", name), nil
}

func introduce() string {
    return "Welcome"
}

然後是測試源碼文件,這裏對文件名和函數名稱都是有要求的。看函數名稱這裏都是功能測試函數:

// demo_test.go
package main

import (
    "fmt"
    "testing"
)

func TestHello(t *testing.T) {
    var name string
    greeting, err := hello(name)
    if err == nil {
        t.Errorf("The error is nil, but it should not be. (name=%q)", name)
    }
    if greeting != "" {
        t.Errorf("Nonempty greeting, but it should not be. (name=%q)", name)
    }
    name = "Clark"
    greeting, err = hello(name)
    if err != nil {
        t.Errorf("The error is not nil, but it should be. (name=%q)", name)
    }
    if greeting == "" {
        t.Errorf("Empty greeting, but it should not be. (name=%q)", name)
    }
    expected := fmt.Sprintf("Hello %s.", name)
    if greeting != expected {
        t.Errorf("greeting: %q, expected: %q, not same.", greeting, expected)
    }
    t.Logf("The expected greeting is %q.\n", expected)
}

func TestIntroduce(t *testing.T) {
    intro := introduce()
    expected := "Welcome"
    if intro != expected {
        t.Errorf("intro: %q, expected: %q, not same", intro, expected)
    }
    t.Logf("The expected introduce is %q.\n", expected)
}

功能測試的結果

先來看下面的測試命令和結果:

PS H:\Go\src> go test Go36\article23\example01
ok      Go36/article23/example01        0.051s
PS H:\Go\src>

在這裏,輸入了命令go test Go36\article23\example01,表示想對這個導入路徑的代碼包進行測試。另外go test命令,默認執行當前目錄下以_test.go結尾的測試文件,所以可以不帶後面的參數。
命令的下面一行,就是此次測試的簡要結果。簡要結果有三塊內容:

  • 最左邊的ok表示此次測試成功
  • 中間的內容,顯示了被測代碼包的導入路徑
  • 最右邊,展現了此次對該代碼包測試所耗的時間

緩存

對於最右邊的時間,有時候可能是這樣的:

ok      Go36/article23/example01        (cached)

這表明,由於測試代碼與被測試代碼都沒有任何變動,所以go test命令直接把之前緩存測試成功的結果打印出來了。關於這個緩存,可以運行下面的命令來查看緩存目錄的路徑:

go env GOCACHE

可能windows系統上沒有緩存?我在win10系統上貌似是沒有緩存。go env裏也沒有GOCACHE這個環境變量。嘗試手動去系統裏加上這個變量,再用go env也是顯示不出來,系統重啟也試過了
緩存的數據可以正確的反應出測試時的各種源碼文件、構建環境、編譯器選項等情況。一旦有任何變動,緩存的數據就會失效,go test命令就會再次真正地執行操作。go命令會定期地刪除最近未使用的緩存數據,不過也是可以手動刪除所有緩存數據的,命令如下:

go clean -cache

上面的命令清除的是go命令所有的緩存,對於測試成功的結果,go命令也會緩存。可以只刪除所有測試結果的緩存,而不刪除任何構建結果的緩存。命令如下:

go clean -testcache

這裏的命令也有問題,打印go clen的幫助如下:

PS H:\Go\src\Go36> go clean --help
usage: clean [-i] [-r] [-n] [-x] [build flags] [packages]

就上面這些參數,這裏的命令參數貌似都是沒有的,不清楚是版本問題,還是系統問題。
這裏是網上查到的方法:顯示禁用緩存的慣用方法是使用-count=1標誌。就是作為參數加到go test命令後面。
總之,不用太在意緩存數據的存在,因為這並不會妨礙go test命令打印正確的測試結果。

測試失敗

如果功能測試的函數的參數名稱為t,那麽調用t.Fail方法,可以使測試失敗。在之前的測試代碼的最後,再新增一個功能測試函數TestFail,具體如下:

func TestFail(t *testing.T) {
    t.Fail()
    t.Log("測試失敗...")
}

現在再運行測試,就會顯示測試失敗,具體如下:

PS G:\Steed\Documents\Go\src\Go36\article24\example01> go test
--- FAIL: TestFail (0.00s)
        demo_test.go:43: 測試失敗...
FAIL
exit status 1
FAIL    Go36/article24/example01        0.115s
PS G:\Steed\Documents\Go\src\Go36\article24\example01>

上面的內容顯示,TestFail函數的測試失敗。
對於失敗測試的結果,是不會進行緩存的,所以每次測試都是全新的結果。

t.FailNow()
這裏還有一個方法:t.FailNow()。
與t.Fail()不同,在t.FailNow()執行後,當前函數會立即終止執行。t.FailNow方法只是終止它所在的函數,不會影響到其他函數的處理。但是當前函數執行到t.FailNow()之後,後面的代碼就沒有執行的的機會了。如果把上面例子中的t.Fail()替換成t.FailNow(),則不會打印測試日誌,因為t.Log沒有機會執行。

測試日誌

如果測試失敗了,那麽go test會將導致失敗的測試函數中的常規測試日誌一並打印出來,就是t.Log方法調用的內容。
t.Log方法以及t.Logf方法,就是打印常規的測試日誌。只有在測試失敗的時候才當測試成功的時候,不打印這類日誌。默認在測試成功的時候不打印這類日誌,不過可以加上-v參數在成功時也打印測試日誌。

t.Error 和 t.Errorf
如果想在測試失敗的通常打印失敗測試日誌,那麽可以直接調用t.Error或t.Errorf方法。相當於t.Log或t.Logf和t.Fail方法的連續調用。

t.Fatal 和 t.Fatalf
這兩個方法的作用是在打印失敗錯誤日誌後立即終止當前測試函數的執行並宣告失敗。就是相當於調用了t.FailNow方法。

性能測試

性能測試與功能測試的結果有很多相似的地方,先把示例代碼準備好,然後關註性能測試結果裏的特殊之處。

測試示例

先準備用來做性能測試的包,這裏用不同的方法來實現斐波那契數列。一個用遞歸方法,效率比較差,還有一個是for循環實現的,邏輯稍微復雜一點但是效率更高:

// Go36/article24/example02/fibonacci/fibonacci.go
package fibonacci

func FibRescuvie (x uint) uint64 {
    if x > 93 { return 0 }  // uint64最多可以存第93個數,再高就溢出了
    switch x {
    case 0:
        return 0
    case 1, 2:
        return 1
    default:
        return FibRescuvie(x-2) + FibRescuvie(x-1)
    }
}

func FibForLoop (x uint) uint64 {
    first := uint64(1)
    second := uint64(1)
    var r uint64
    for i := uint(3); i <= x; i++ {
        r = first + second
        first, second = second, r
    }
    return r
}

然後是性能測試的代碼:

// Go36/article24/example02/fibonacci/fibonacci_test.go
package fibonacci

import "testing"

func BenchmarkFibRescuvie(b *testing.B) {
    for i := 0; i < b.N; i++ {
        FibRescuvie(40)
    }
}

func BenchmarkFibForLoop(b *testing.B) {
    for i := 0; i < b.N; i++ {
        FibForLoop(40)
    }
}

接著就來運行測試:

PS H:\Go\src> go test -bench . -run=^$ Go36/article24/example02/fibonacci
goos: windows
goarch: amd64
pkg: Go36/article24/example02/fibonacci
BenchmarkFibRescuvie-8                 3         408665433 ns/op
BenchmarkFibForLoop-8           100000000               19.2 ns/op
PASS
ok      Go36/article24/example02/fibonacci      4.484s
PS H:\Go\src>

性能測試的命令

先說運行測試的命令,上面的命令加了2個標記:
-bench .,必須要加上這個標記,才會進行性能測試。該標記的值是個點(.)表示要執行的性能測試函數,這裏用了點,表示執行任意名稱的性能測試函數。還可以這麽寫-bench="."-bench FibRescuvie-bench=FibRescuvie
-run=^$,這個標記由於表明需要執行哪些功能測試函數,同樣也是以函數名稱為依據。這裏標記的值是^$,分別是正則表達式的開頭和結束符,就是一個空的名稱。也就是沒有任何函數名稱會符合要求,就是不執行任何的功能測試函數。如果不加-run標記,那麽就會運行代碼包中所有的功能測試函數。
這裏的兩個標記的值都是正則表達式,並且也只能以正則表達式為值。

性能測試結果

關於結果,重點說下這行,重新貼出來:

BenchmarkFibRescuvie-8                 3         408665433 ns/op

CPU
BenchmarkFibRescuvie-8被稱為單個性能測試的名稱。它表示命令執行了性能測試函數BenchmarkFibRescuvie,並且當時所有的最大P數量為8。這裏的P是processor的縮寫,最大P數量相當於可以同時運行goroutine的邏輯CPU的最大個數(確認了一下,機器確實是8核的)。這裏的邏輯CPU,也可以被稱為CPU核心,但它並不等同於計算機中真正的CPU核心,只是Go語言運行時系統內部的一個概念,代表著它同時運行goroutine的能力。一臺計算機的CPU核心的個數,意味著它能在同一時刻執行多少條程序指令,代表著它並行處理程序指令的能力。可以通過調用runtime.GOMAXPROCS函數改變最大P數量,也可以在運行go tes命令的時候,加入標記-cpu來設置一個最大P數量的列表,以供命令在多次測試時使用。

執行次數
中間的數字是被測試函數被執行的實際次數。go test命令在執行性能測試函數的時候會給它一個正整數,例子中就是b.N。我們應該在測試函數中配合著編寫代碼,比如:

for i := 0; i < b.N; i++ {
    FibForLoop(40)
}

它會先叠代b.N次的循環中調用被測試函數。go test命令先嘗試把b.N設置為1,然後執行測試函數。如果測試函數的執行時間沒有超過上限,上限默認為1秒,那麽命令就會改大b.N的值,然後再次執行測試函數,如此往復,知道這個時間大於或等於上限為止。可以簡稱該值為執行次數,但是它指的是被測試函數的執行次數,而不是性能測試函數的執行次數。

運行平均時間
最後是遊標的數字,表明單次執行被測試函數的平均耗時,這裏的ns是納秒。這個值其實就是將測試的總時間除以執行次數而得出的。

go test命令

介紹幾個可以被go test命令接受的重要標記,進一步說明功能測試和性能測試在不同條件下的測試流程。

回顧G-P-M模型

上篇的go test命令提到過一個-cpu標記,它是用來設置執行最大P數量的列表的。
這裏的P是process的縮寫,在講go語句(就是goroutine)的時候說過。每個process都是一個可承載若幹個G,並且能夠使這些G適時地與M進行對接並得到真正運行的中介。正式由於P的存在,G和M才可以呈現出多對多的關系,並能夠及時、靈活地進行組合和分離。
這裏的G是goroutine的縮寫,可以被理解為Go語言自己實現的用戶級線程。
這裏的M是machine的縮寫,代表著系統級線程,或者說操作系統內核級別的線程。
Go語言並發程序模型中的P,正式goroutine的數量能夠數十萬計的關鍵所在。P的數量意味著Go程序背後運行時系統中,會有多少個由於承載可運行的G的隊列存在。每一個隊列都相當於一條流水線,源源不斷地把可運行的G輸送給空閑的M,並使兩者對接。一旦對接完成,被對接的G就真正的運行在操作系統的內核級線程之上了。每條流水線之間雖然有聯系,但都是獨立運作的。
因此,最大P數量就代表著Go語言運行時系統同時運行goroutine的能力,也可以被視為其中邏輯CPU的最大個數。go test命令的-cpu標記就是用於設置這個最大個數的。在默認情況下,最大P數量就等於當前計算機CPU核心的實際數量。也可以大於或者小於實際數量,如此可以在一定程度上模擬擁有不同CPU核心數的計算機。

設置-cpu標記的值

標記-cpu應該是一個正整數的列表,用逗號分隔的多個整數字面量。
針對值中的每一個正整數,go test會先設置最大P數量為該數值,然後再執行測試函數。如果測試函數有多個,go test會先以所有-cpu參數值去執行第一個測試函數,然後再用同樣的方式執行後面的測試函數。
go test執行測試函數時,關於-cpu標記的具體步驟。不論是否設置了-cpu標記,go test在進行準備工作的時候,會讀取-cpu標記的值,並把它轉換為一個int為元素類型的切片。如果沒有-cpu標記,那麽切片就只包含一個元素,值是最大P數量的默認值。在準備某個測試函數的時候,無論該函數是功能測試函數還是性能測試函數,go test會叠代切片的值。在每次叠代的時候,先依據當前元素的值設置最大P數量,然後再去執行測試函數。
這裏註意,雖然上一小節說可以大於CPU核心的實際數量。但是最好不要超過,因為一旦超出計算機實際的並行處理能力,Go程序在性能上就無法再獲得到顯著的提升了。而且為了管理過多的P,系統還會耗費額外的計算資源。

-count標記

go test每一次對性能測試函數的執行,對一個探索的過程。它會在測試函數的執行時間上限不變的前提下,嘗試找到被測試程序的最大執行次數。在這個過程中,性能測出函數可能會被執行多次。可以把這樣一個探索的過程稱為探索式執行
這裏有一個標記-count,就是專門用於重復執行測試函數的。它的值必須大於或等於0,默認值為1。如果設置了-count,對於每一個測試函數,命令都會在設置不同的條件下(比如不同的最大P數量)分別重復執行若幹次。所以如果配合上-cpu標記,那麽執行的次數就是-cpu切片裏元素的數量和-count數值的乘積,這個是對於功能測試的執行次數。如果是性能測試,這個乘積就是測試函數執行探索式執行的次數,實際次數就要再乘以一次探索式執行中測試函數實際執行的次數了。

下面的結果是設置了-cpu和-count之後的測試結果,可以看下期中的執行次數和順序:

PS G:\Steed\Documents\Go\src\Go36\article24\example02\fibonacci> go test -bench . -cpu 1,2,3 -count 2
goos: windows
goarch: amd64
pkg: Go36/article24/example02/fibonacci
BenchmarkFibRescuvie                   2         642132400 ns/op
BenchmarkFibRescuvie                   2         639623600 ns/op
BenchmarkFibRescuvie-2                 2         637135800 ns/op
BenchmarkFibRescuvie-2                 2         637134900 ns/op
BenchmarkFibRescuvie-3                 2         632637750 ns/op
BenchmarkFibRescuvie-3                 2         630628650 ns/op
BenchmarkFibForLoop             50000000                31.0 ns/op
BenchmarkFibForLoop             50000000                31.1 ns/op
BenchmarkFibForLoop-2           50000000                30.7 ns/op
BenchmarkFibForLoop-2           50000000                31.3 ns/op
BenchmarkFibForLoop-3           50000000                31.0 ns/op
BenchmarkFibForLoop-3           50000000                31.3 ns/op
PASS
ok      Go36/article24/example02/fibonacci      22.063s
PS G:\Steed\Documents\Go\src\Go36\article24\example02\fibonacci>

-parallel標記

在運行go test命令的時候,可以追加-parallel標記。該標記的作用是,設置同一個被測試代碼包中的功能測試函數的最大並發執行數,默認值是測試運行時的最大P數量。
對於功能測試,為了加快測試速度,通常會並發的測試多個被測代碼包。但是默認情況下,同一個代碼包這的過個功能測試函數是串行的執行的。除非在一些功能測試函數中顯式的調用t.Parallel方法。實際也很簡單,在函數開頭加一行:

func TestIntroduce(t *testing.T) {
    t.Parallel()  // 其它不變,就加這一句
    intro := introduce()
    expected := "Welcome"
    if intro != expected {
        t.Errorf("intro: %q, expected: %q, not same", intro, expected)
    }
    t.Logf("The expected introduce is %q.\n", expected)
}

這個時候,這些包含了t.Parallel方法調用的功能測試函數就會被go test並發的執行,而並發執行的最大數量是用-parallel標記決定的。不過同一個功能測試函數的多次執行之間一定是串行的。
另外還可以在一個功能測試函數裏,並發的起多個子測試。下面是一段代碼的示例:

// 這個是要展示的函數
func TestGetPrimesParallel(t *testing.T) {
    for _, max := range []int{1, 2, 3, 4, 5} {
        max := max * 200
        // 這些子測試會被並發地執行,
        // 並且只有它們都執行完畢之後當前的測試函數才會執行完成。
        // 當前的測試函數並不會與其他測試函數一起被並發的執行。
        t.Run(fmt.Sprintf("TestGetPrimesWith%d", max),
            func(t *testing.T) {
                t.Parallel()
                primes := GetPrimes(max)
                err := comparePrimes(primes)
                if err != nil {
                    t.Error(err)
                } else {
                    t.Logf("The primes less than %d are all correct.", max)
                }
            }
        )
    }
}

// 下面的切片和一個函數也是測試源碼文件裏要用到的部分
var expectedPrimes = []int{
    2, 3, 5, 7, 11, 13, 17, 19,
    23, 29, 31, 37, 41, 43, 47, 53,
    59, 61, 67, 71, 73, 79, 83, 89,
    97, 101, 103, 107, 109, 113, 127, 131,
    137, 139, 149, 151, 157, 163, 167, 173,
    179, 181, 191, 193, 197, 199, 211, 223,
    227, 229, 233, 239, 241, 251, 257, 263,
    269, 271, 277, 281, 283, 293, 307, 311,
    313, 317, 331, 337, 347, 349, 353, 359,
    367, 373, 379, 383, 389, 397, 401, 409,
    419, 421, 431, 433, 439, 443, 449, 457,
    461, 463, 467, 479, 487, 491, 499, 503,
    509, 521, 523, 541, 547, 557, 563, 569,
    571, 577, 587, 593, 599, 601, 607, 613,
    617, 619, 631, 641, 643, 647, 653, 659,
    661, 673, 677, 683, 691, 701, 709, 719,
    727, 733, 739, 743, 751, 757, 761, 769,
    773, 787, 797, 809, 811, 821, 823, 827,
    829, 839, 853, 857, 859, 863, 877, 881,
    883, 887, 907, 911, 919, 929, 937, 941,
    947, 953, 967, 971, 977, 983, 991, 997,
}

func comparePrimes(primes []int) (err error) {
    for i, prime := range primes {
        expectedPrime := expectedPrimes[i]
        if prime != expectedPrime {
            err = fmt.Errorf(
                "%dth prime number %d is not the expected value %d",
                i, prime, expectedPrime)
            break
        }
    }
    return
}

// 下面是命令源碼文件裏被測試的函數
// GetPrimes 用於獲取小於或等於參數max的所有質數。
// 本函數使用的是愛拉托遜斯篩選法(Sieve Of Eratosthenes)。
func GetPrimes(max int) []int {
    if max <= 1 {
        return []int{}
    }
    marks := make([]bool, max)
    var count int
    squareRoot := int(math.Sqrt(float64(max)))
    for i := 2; i <= squareRoot; i++ {
        if marks[i] == false {
            for j := i * i; j < max; j += i {
                if marks[j] == false {
                    marks[j] = true
                    count++
                }
            }
        }
    }
    primes := make([]int, 0, max-count)
    for i := 2; i < max; i++ {
        if marks[i] == false {
            primes = append(primes, i)
        }
    }
    return primes
}

上面的代碼中,由於測試函數本身,就是TestGetPrimesParallel這個函數裏沒有顯式的調用t.Parallel方法,所以這個測試函數是不會與別的測試函數一起被並發的執行的。並發執行的只是裏面的那些顯式的調用了t.Parallel方法的子測試。
另外,這個標記對性能測試是無效的。對於性能測試來說,也是可以並發進行的,不過機制上不同。這涉及了b.RunParallel方法、b.SetParallelism方法和-cpu標記的聯合運用,這裏沒有展開講。要進一步了解,建議去看testing包的文檔了:https://golang.google.cn/pkg/testing/

性能測試函數中的計時器

如果去查看testing包的文檔,在裏面會發現其中的testing.B類型有這麽幾個指針方法:

  • func (b *B) StartTimer()
  • func (b *B) StopTimer()
  • func (b *B) ResetTimer()

這些方法都是用於操作當前性能測試函數專屬的計時器的。這三個方法在開始記錄、停止記錄以及重新記錄執行時間的同時,也會對堆內存分配字節數和分配次數的記錄起到相同的作用。
看下面的例子,在之前的性能測試函數之前加了4行代碼:

func BenchmarkFibForLoop(b *testing.B) {
    b.StopTimer()
    time.Sleep(time.Millisecond * 300)  // 模擬一個費時的操作
    var n uint = 40
    b.StartTimer()
    for i := 0; i < b.N; i++ {
        FibForLoop(n)
    }
}

這裏,先停止了計時器。然後time.Sleep,接著給n賦值,這裏其實是模擬了一個比較耗時的額外操作,為的是獲取到n的值。然後又啟動了計時器。這裏的意義是在為n賦值的這個過程中,計時器是停止的。所以這部分時間不會包含在測試函數的執行時間中,這樣就避免了這個耗時的賦值過程對測試結果的影響。這裏被測試程序在執行時間限制內,對被測出函數的最大執行次數不會受到這個段停止計時的時間的影響。可以註釋掉b.StopTimer()和b.StartTimer()比較性能測試的結果,主要看運行次數:

PS G:\Steed\Documents\Go\src\Go36\article25\example01\fibonacci> go test -bench FibForLoop
goos: windows
goarch: amd64
pkg: Go36/article25/example01/fibonacci
BenchmarkFibForLoop-4           30000000                40.5 ns/op
PASS
ok      Go36/article25/example01/fibonacci      6.376s
PS G:\Steed\Documents\Go\src\Go36\article25\example01\fibonacci> go test -bench FibForLoop
goos: windows
goarch: amd64
pkg: Go36/article25/example01/fibonacci
BenchmarkFibForLoop-4           50000000                33.9 ns/op
PASS
ok      Go36/article25/example01/fibonacci      3.366s
PS G:\Steed\Documents\Go\src\Go36\article25\example01\fibonacci>

下面的結果是沒有註釋代碼時執行的結果,和之前測試的結果是一樣的。上面的結果是註釋掉b.StopTimer()和b.StartTimer()之後執行的,由於每次為n賦值都會耗費額外的時間,並且計算在測試函數的執行時間裏了,所以單次執行的時間長了,總的執行次數也就變少了。
在性能測試函數中,可以通過b.StopTimer方法和b.StartTimer方法的聯合運用,去除掉任何一段代碼的執行時間。還有一個b.ResetTimer方法,靈活性就要差一些,值能用於去除在調用它之前的那些代碼的執行時間。不過,無論在調用它的時候,計時器是否正在運轉,它都可以起作用。

其它標記

最後還有2個標記是通過思考題給出的,所以沒有詳細的介紹:

  • -benchmem : 輸出性能測試的內存分配統計信息。
  • -benchtime : 用於指定性能測試的探索式測試執行時間上限

命令示例:

$ go test -bench . -benchmem -benchtime 10s

Go36-23,24,25-單元測試