溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

gopl 測試

發(fā)布時間:2020-06-13 19:01:20 來源:網(wǎng)絡 閱讀:1128 作者:騎士救兵 欄目:編程語言

go test

測試是自動化測試的簡稱,即編寫簡單的程序來確保程序(產(chǎn)品代碼)在該測試中針對特定輸入產(chǎn)生預期的輸出。這些測試主要分兩種:

  • 通過精心設計,用來檢測某種功能
  • 隨機性的,用來擴大測試的覆蓋面

go test 子命令是 Go 語言包的測試驅(qū)動程序。在一個包目錄中,以 _test.go 結(jié)尾的文件不是 go build 命令編譯的目標,而是 go test 編譯的目標。
在 *_test.go 的測試源碼文件中,有三種類型的函數(shù):

  • 功能測試函數(shù)
  • 基準測試函數(shù)
  • 示例函數(shù)

功能測試函數(shù),以 Test 開頭,用來檢測一些程序邏輯的正確性。
基準測試函數(shù),以 Benchmark 開頭,用來測試程序的性能。
示例函數(shù),以 Example 開頭,提供一個機器檢查過的示例文檔。

Test 函數(shù)(功能測試)

每一個測試文件必須導入 testing 包。這些函數(shù)的函數(shù)簽名如下:

func TestName(t *testing.T) {
    // ...
}

參數(shù) t 提供了匯報測試失敗和日志記錄的功能。

定義示例

下面先定義一個用來測試的示例,這個示例包含一個函數(shù) IsPalindrome,用來判斷一個字符串是否是回文:

// word 包提供了文字游戲相關的工具函數(shù)
package word

// IsPalindrome 判斷一個字符串是否是回文
func IsPalindrome(s string) bool {
    for i := range s {
        if s[i] != s[len(s)-1-i] {
            return false
        }
    }
    return true
}

這個函數(shù)對于一個字符串是否是回文字符串前后重復測試了兩次,其實只要檢查完字符串一半的字符就可以結(jié)束了。這個在稍后測試性能的時候會做改進,這里先關注功能。

測試源碼文件

在同一個目錄中,再寫一個測試文件。假設上面的示例的文件名是 word.go,那么這個測試文件的文件名可以是 word_test.go(命名沒有強制要求,但是這樣的命名使得文件的意義一目了然)。文件中包含了兩個功能測試函數(shù),這兩個函數(shù)都是檢查 IsPalindrome 函數(shù)是否針對某個輸入的參數(shù)能給出正確的結(jié)果,并且用 t.Error 來報錯:

package word

import "testing"

func TestPalindrome(t *testing.T) {
    if !IsPalindrome("civic") {
        t.Error(`IsPalindrome("civic") = false`)
    }
    if !IsPalindrome("madam") {
        t.Error(`IsPalindrome("madam") = false`)
    }
}

func TestNonPalindrome(t *testing.T) {
    if IsPalindrome("palindrome") {
        t.Error(`IsPalindrome("palindrome") = true`)
    }
}

功能擴展

這個最初版本的回文判斷函數(shù)比較簡陋,有些明顯也是回文的情況,但是無法被現(xiàn)在這個版本的函數(shù)檢測出來:

  • "上海自來水來自海上"
  • "Madam, I'm Adam"

針對上面兩種回文,又寫了新的測試用例:

func TestChinesePalindrome(t *testing.T) {
    input := "上海自來水來自海上"
    if !IsPalindrome(input) {
        t.Errorf(`IsPalindrome(%q) = false`, input)
    }
}

func TestSentencePalindrome(t *testing.T) {
    input := "Madam, I'm Adam"
    if !IsPalindrome(input) {
        t.Errorf(`IsPalindrome(%q) = false`, input)
    }
}

這里用了 Errorf 函數(shù),具有格式化的功能。

運行 go test

添加了新的測試后,再運行 go test 命令失敗了,錯誤信息如下:

PS G:\Steed\Documents\Go\src\gopl\ch21\word1> go test
--- FAIL: TestChinesePalindrome (0.00s)
    word_test.go:23: IsPalindrome("上海自來水來自海上") = false
--- FAIL: TestSentencePalindrome (0.00s)
    word_test.go:30: IsPalindrome("Madam, I'm Adam") = false
FAIL
exit status 1
FAIL    gopl/ch21/word1 0.292s
PS G:\Steed\Documents\Go\src\gopl\ch21\word1>

這里是一個比較好的實踐,先寫測試然后發(fā)現(xiàn)它觸發(fā)的的錯誤。通過這步,可以定位到真正要解決的問題,并在修復后確認問題已經(jīng)解決。

運行 go test 還可以指定一些參數(shù):

PS G:\Steed\Documents\Go\src\gopl\ch21\word1> go test -v -run="Chinese|Sentence"
=== RUN   TestChinesePalindrome
--- FAIL: TestChinesePalindrome (0.00s)
    word_test.go:23: IsPalindrome("上海自來水來自海上") = false
=== RUN   TestSentencePalindrome
--- FAIL: TestSentencePalindrome (0.00s)
    word_test.go:30: IsPalindrome("Madam, I'm Adam") = false
FAIL
exit status 1
FAIL    gopl/ch21/word1 0.250s
PS G:\Steed\Documents\Go\src\gopl\ch21\word1>

參數(shù) -v 可以輸出包中每個測試用例的名稱和執(zhí)行時間。默認只會輸出有問題的測試。
參數(shù) -run 是一個正則表達式,可以使 go test 只運行那些測出函數(shù)名稱匹配的函數(shù)。

上面選擇性地只運行新的測試用例。一旦之后的修復使得測試用例通過后,還必須使用不帶開關的 go test 來運行一次完整的測試。

新的示例函數(shù)

上一版本的函數(shù)比較簡單,使用字節(jié)序列而不是字符序列,因此無法支持非 ASCII 字符的檢查。另外也沒有忽略空格、標點符號和字母大小寫。下面重寫了這個函數(shù):

// word 包提供了文字游戲相關的工具函數(shù)
package word

import "unicode"

// IsPalindrome 判斷一個字符串是否是回文
func IsPalindrome(s string) bool {
    var letters []rune
    for _, r := range s {
        if unicode.IsLetter(r) {
            letters = append(letters, unicode.ToLower(r))
        }
    }
    for i := range letters {
        if letters[i] != letters[len(letters)-1-i] {
            return false
        }
    }
    return true
}

新的測試用例

測試用例也重新寫。這里是一個更加全面的測試用例,把之前的用例和新的用例結(jié)合到一個表里:

package word

import "testing"

func TestIsPalindrome(t *testing.T) {
    var tests = []struct {
        input string
        want  bool
    }{
        {"", true},
        {"a", true},
        {"aa", true},
        {"ab", false},
        {"kayak", true},
        {"palindrome", false},
        {"desserts", false},
        {"上海自來水來自海上", true},
        {"Madam, I'm Adam", true},
    }
    for _, test := range tests {
        if got := IsPalindrome(test.input); got != test.want {
            t.Errorf(`IsPalindrome(%q) = %v`, test.input, got)
        }
    }
}

這種基于表的測試方式在 Go 里面很常見。根據(jù)需要添加新的表項很直觀,并且由于斷言邏輯沒有重復,因此可以花點精力讓輸出的錯誤消息更好看一點。

小結(jié)-測試函數(shù)

調(diào)用 t.Errorf 輸出的失敗的測試用例信息沒有包含整個跟蹤棧信息,也不會導致程序終止執(zhí)行。這樣可以在一次測試過程中發(fā)現(xiàn)多個失敗的情況。
如果需要在測試函數(shù)中終止,比如由于初始化代碼失敗,可以使用 t.Fatal 或 t.Fatalf 函數(shù)來終止當前測試函數(shù),它們必須在測試函數(shù)的同一個 goroutine 內(nèi)調(diào)用。

測試錯誤消息的建議
測試錯誤消息一般格式是 f(x)=y, want z,這里 f(x) 表示需要執(zhí)行的操作和它的輸入,y 是實際的輸出結(jié)果,z 是期望得到的結(jié)果。在測試一個布爾函數(shù)的時候,省略 “want z” 部分,因為它沒有給出有用的信息。上面的測試用例輸出的錯誤消息基本也是這么做的,

隨機測試

基于表的測試方便針對精心選擇的輸入檢測函數(shù)是否工作正常,以測試邏輯上引人關注的用例。另外一種方式是隨機測試,通過構(gòu)建隨機輸入來擴展測試的覆蓋范圍。
對于隨機的輸入,要如何確認輸出是否正確,這里有兩種策略:

  • 額外寫一個函數(shù),這個函數(shù)使用低效但是清晰的算法,然后檢查兩種實現(xiàn)的輸出是否一致
  • 構(gòu)建符合某種模式的輸入,這樣就可以知道期望的輸出模式

下面的例子使用了第二種模式,randomPalindrome 函數(shù)可以隨機的創(chuàng)建回文字符串,使用這些回文字符串來驗證進行測試:

import (
    "math/rand"
    "testing"
    "time"
)

// randomPalindrome 返回一個回文字符串,它的長度和內(nèi)容都是隨機生成的
func randomPalindrome(rng *rand.Rand) string {
    n := rng.Intn(25) // 隨機字符串最大長度24
    runes := make([]rune, n)
    for i := 0; i < (n+1)/2; i++ {
        r := rune(rng.Intn(0x1000)) // 隨機字符最大是 `\u0999
        runes[i] = r
        runes[n-1-i] = r
    }
    return string(runes)
}

func TestRandomPalindromes(t *testing.T) {
    seed := time.Now().UTC().UnixNano()
    t.Logf("Random seed: %d", seed)
    rng := rand.New(rand.NewSource(seed))
    for i := 0; i < 1000; i++ {
        p := randomPalindrome(rng)
        if !IsPalindrome(p) {
            t.Errorf("IsPalindrome(%q) = false", p)
        }
    }
}

由于隨機測試的不確定性,在遇到測試用例失敗的情況下,一定要記錄足夠多的信息以便于重現(xiàn)這個問題。這里記錄偽隨機數(shù)生成的種子會比轉(zhuǎn)存儲整個輸入數(shù)據(jù)結(jié)構(gòu)要簡單得多。有了隨機數(shù)的種子,就可以簡單地修改測試代碼來準確地重現(xiàn)錯誤。
通過使用當前時間作為偽隨機數(shù)的種子源,在測試的整個生命周期中,每次運行的時候都會得到新的輸入。如果你的項目使用自動化系統(tǒng)來周期地運行測試,這一點很重要。

測試一個命令

就是測試命令源碼文件,其實和測試包源碼文件差不多。畢竟都是一樣的代碼,不過需要額外做一些特殊的處理。
對于包的測試,go test 很有用,但是稍加修改,也能夠?qū)⑺脕頊y試可執(zhí)行程序。一個 main 包可以生成可執(zhí)行程序,不過也可以當做庫來導入。

示例程序

下面的 echo 程序,可以輸出命令行參數(shù):

// 輸出命令行參數(shù)
package main

import (
    "flag"
    "fmt"
    "strings"
)

var n = flag.Bool("n", false, "omit trailing newline")
var sep = flag.String("s", " ", "separator")

func main() {
    flag.Parse()
    fmt.Print(strings.Join(flag.Args(), *sep))
    if !*n {
        fmt.Println()
    }
}

為了便于測試,需要對程序進行修改。把程序分成兩個函數(shù),echo 執(zhí)行邏輯,main 用來讀取和解析命令行參數(shù)以及報告 echo 函數(shù)可能返回的錯誤:

// 輸出命令行參數(shù)
package main

import (
    "flag"
    "fmt"
    "io"
    "os"
    "strings"
)

var (
    n   = flag.Bool("n", false, "omit trailing newline")
    sep = flag.String("s", " ", "separator")
)

var out io.Writer = os.Stdout // 測試過程中將會被更改

func main() {
    flag.Parse()
    if err := echo(!*n, *sep, flag.Args()); err != nil {
        fmt.Fprintf(os.Stderr, "echo: %v\n", err)
        os.Exit(1)
    }
}

func echo(newline bool, sep string, args []string) error {
    fmt.Fprintf(out, strings.Join(args, sep))
    if newline {
        fmt.Fprintln(out)
    }
    return nil
}

分離出執(zhí)行邏輯
把程序的主要功能從 main 函數(shù)里分離出來了,運行程序的時候通過 main 函數(shù)來調(diào)用 echo。而測試的時候,就可以直接對 echo 函數(shù)進行測試。
避免依賴全局變量
在接下來的測試中,將通過不同的參數(shù)和開關來調(diào)用 echo,以檢查它在不同的模式下都能正常工作。這里的 echo 函數(shù)調(diào)用的時候,通過傳參獲取這些信息,這是為了避免函數(shù)依賴全局變量,這樣測試的時候也可以直接傳參來調(diào)用 echo 不同的模式。
控制輸出的變量
這里還另外引入了一個全局變量 out,該變量是 io.Writer 類型,所有的結(jié)果都將輸出到這里。echo 函數(shù)的輸出是輸出到 out 變量而不是直接輸出到 os.Stdout。這樣正常使用的時候,就是輸出到用戶界面,而測試的時候,可以覆蓋掉這個變量輸出到其他地方。這樣是實現(xiàn)了記錄寫入的內(nèi)容以便于檢查。

測試代碼

下面是測試代碼,在文件 echo_test.go 中:

package main

import (
    "bytes"
    "fmt"
    "testing"
)

func TestEcho(t *testing.T) {
    var tests = []struct {
        newline bool
        sep     string
        args    []string
        want    string
    }{
        {true, "", []string{}, "\n"},
        {false, "", []string{}, ""},
        {true, "\t", []string{"one", "two", "three"}, "one\ttwo\tthree\n"},
        {true, ",", []string{"a", "b", "c"}, "a,b,c\n"},
        {false, ":", []string{"1", "2", "3"}, "1:2:3"},
    }
    for _, test := range tests {
        descr := fmt.Sprintf("echo(%v, %q, %q)", test.newline, test.sep, test.args)
        out = new(bytes.Buffer) // 捕獲的輸出
        if err := echo(test.newline, test.sep, test.args); err != nil {
            t.Errorf("%s failed: %v", descr, err)
            continue
        }
        got := out.(*bytes.Buffer).String()
        if got != test.want {
            t.Errorf("%s = %q, want %q", descr, got, test.want)
        }
    }
}

這里依然是通過表來組織測試用例,這樣可以很容易地添加新的測試用例。下面是添加了一行到測試用例中:

{false, ":", []string{"1", "2", "3"}, "1:2:3\n"},

上面添加的這條是有錯誤的,正好可以看看測試失敗的時候的輸出:

PS H:\Go\src\gopl\ch21\echo> go test
--- FAIL: TestEcho (0.00s)
    echo_test.go:32: echo(false, ":", ["1" "2" "3"]) = "1:2:3", want "1:2:3\n"
FAIL
exit status 1
FAIL    gopl/ch21/echo  0.163s
PS H:\Go\src\gopl\ch21\echo>

錯誤信息首先描述了想要進行的操作,使用了類似 Go 的語法,就像一個函數(shù)調(diào)用。然后依次是實際獲得個值和預期的結(jié)果。這樣的錯誤信息就很有幫助。

測試中的錯誤處理
還要注意,測試代碼里并沒有調(diào)用 log.Fatal 或 os.Exit,因為這兩個調(diào)用會阻止跟蹤的過程,這兩個函數(shù)的調(diào)用可以認為是 main 函數(shù)的特權。如果有時候發(fā)生了未預期的錯誤或者崩潰,即使測試用例本身失敗了,測試驅(qū)動程序也還可以繼續(xù)工作。預期的的錯誤應該通過返回一個非空的 error 值來報告,就像上面的測試代碼里做的那樣。

白盒測試

測試的一種分類方式是基于對所要進行測試的包的內(nèi)部的了解程度:

  • 黑盒測試,假設測試者對包的了解僅通過公開的API和文檔,而包的內(nèi)部邏輯是不透明的
  • 白盒測試,可以訪問包的內(nèi)部函數(shù)和數(shù)據(jù)結(jié)構(gòu),并且可以做一些常規(guī)用戶無法做到的觀察和改動

白盒這個名字是傳統(tǒng)的說法,凈盒(clear box)的說法更準確。
以上兩種方法是互補的。黑盒測試通常更加健壯,程序更新后基本不需要修改。并且可以幫助測試者了解用戶的情況以及發(fā)現(xiàn)API設計的缺陷。反之,白盒測試可以對實現(xiàn)的特定之處提供更詳細的覆蓋測試。
之前的內(nèi)容已經(jīng)分別給出了這兩種測試方法的例子:

  • TestIsPalindrome 函數(shù)僅調(diào)用導出的函數(shù) IsPalindrome,所以它是一個黑盒測試
  • TestEcho 函數(shù)調(diào)用 echo 函數(shù)并且更新了全局變量 out,無論函數(shù) echo 還是變量 out 都是未導出的,所以它是一個白盒測試

偽實現(xiàn)
在寫 TestEcho 的時候,通過修改 echo 函數(shù),從而在輸出結(jié)果時使用了一個包級別的變量,使得測試可以使用一個額外的實現(xiàn)代替標準輸出來記錄要檢查的數(shù)據(jù)。通過這樣的技術,可以使用易于測試的偽實現(xiàn)來替換部分產(chǎn)品代碼。這種偽實現(xiàn)的優(yōu)點是更易于配置、預測和觀察,并且更可靠。

示例程序

下面的代碼演示了向用戶提供存儲服務的 Web 服務中的限額邏輯。當用戶使用的額度超過 90% 的時候,系統(tǒng)自動發(fā)送一封告警郵件:

package storage

import (
    "fmt"
    "log"
    "net/smtp"
)

var usage = make(map[string]int64)

func bytesInUse(username string) int64 { return usage[username] }

// 郵件發(fā)送者配置
// 注意:永遠不要把密碼放到源代碼中
const sender = "notifications@example.com"
const password = "password"
const hostname = "smtp.example.com"

const template = `Warning: you are using %d bytes of storage,
%d%% of your quota.`

func CheckQuota(username string) {
    used := bytesInUse(username)
    const quota = 1000000000 // 1GB
    percent := 100 * used / quota
    if percent < 90 {
        return // OK
    }
    msg := fmt.Sprintf(template, used, percent)
    auth := smtp.PlainAuth("", sender, password, hostname)
    err := smtp.SendMail(hostname+":587", auth, sender,
        []string{username}, []byte(msg))
    if err != nil {
        log.Printf("smtp.SendMail(%s) failed: %s", username, err)
    }
}

現(xiàn)在想要測試上面的功能,但是并不想真的發(fā)送郵件。所以要把發(fā)送郵件的邏輯移動到獨立的函數(shù)中,并且把它存儲到一個不可導出的變量 notifyUser 中:

var notifyUser = func(username, msg string) {
    auth := smtp.PlainAuth("", sender, password, hostname)
    err := smtp.SendMail(hostname+":587", auth, sender,
        []string{username}, []byte(msg))
    if err != nil {
        log.Printf("smtp.SendMail(%s) failed: %s", username, err)
    }
}

func CheckQuota(username string) {
    used := bytesInUse(username)
    const quota = 1000000000 // 1GB
    percent := 100 * used / quota
    if percent < 90 {
        return // OK
    }
    msg := fmt.Sprintf(template, used, percent)
    notifyUser(username, msg)
}

現(xiàn)在可以寫測試了。

測試代碼

下面是一個簡單的測試,這個測試用偽造的通知機制而不是真的發(fā)送郵件。這個測試會記錄下需要通知的用戶和通知的內(nèi)容,并驗證是否符合期望:

package storage

import (
    "strings"
    "testing"
)

func TestCheckQuotaNotifiesUser(t *testing.T) {
    var notifiedUser, notifiedMsg string
    notifyUser = func(user, msg string) {
        notifiedUser, notifiedMsg = user, msg
    }

    const user = "steed@example.org"
    usage[user] = 980000000 // 模擬已經(jīng)使用了 980M 的情況

    CheckQuota(user)
    if notifiedUser == "" && notifiedMsg == "" {
        t.Fatalf("notifyUser not called") // 比如沒有超過限額,就會進入這個分支
    }
    if notifiedUser != user {
        t.Errorf("wrong user (%s) notified, want %s", notifiedUser, user)
    }
    const wantSubstring = "98% of your quota"
    if !strings.Contains(notifiedMsg, wantSubstring) {
        t.Errorf("unexpected notification message <<%s>>, want substring %q", notifiedMsg, wantSubstring)
    }
}

正確使用偽實現(xiàn)

目前來看,這個測試本身完成的很好,但是還有一個遺留問題。因為對 CheckQuota 測試中使用了偽實現(xiàn)替換了原本的 notifyUser 的內(nèi)容,這樣在之后的其他測試中,notifyUser 依然是這里被替換上的偽實現(xiàn),這可能使得其他的測試無法正常工作(對于全局變量的更新一直都是存在風險的)。這里還必須再修改一下這個測試讓他最后可以恢復 notifyUser 原來的值,這樣之后的測試就不會收到影響。這里必須在所有的測試執(zhí)行路徑上這樣做,包括測試失敗和崩潰的情況。通常這種情況下建議使用 defer :

func TestCheckQuotaNotifiesUser(t *testing.T) {
    // 保存留待恢復的notifyUser
    saved := notifyUser
    defer func() { notifyUser = saved }()

    // 設置測試的偽通知notifyUser
    var notifiedUser, notifiedMsg string
    notifyUser = func(user, msg string) {
        notifiedUser, notifiedMsg = user, msg
    }

    // ...測試其余的部分...
}

以這種方式來使用全局變量是安全的,因為 go test 一般不會并發(fā)執(zhí)行多個測試。
這種方式有很多用處:

  • 用來臨時保存并恢復各種全局變量,包括命令行標志、調(diào)試參數(shù)、以及性能參數(shù)
  • 用來安裝和移除鉤子程序來讓產(chǎn)品代碼調(diào)用測試代碼
  • 將產(chǎn)品代碼設置為少見卻很重要的狀態(tài),比如超時、錯誤,甚至是交叉并行執(zhí)行

外部測試包

先來看一下 net/url 包,這個包提供了 URL 解析的功能。還有 net/http 包,這個包提供了 Web 服務器和 HTTP 客戶端的庫。高級的 net/http 包依賴于低級的 net/url 包。然而,在 net/url 包中有一個測試是用來演示 URL 和 HTTP 庫之間進行交互的例子。也就是說,低級別包的測試導入了高級別包。這種情況下,在 net/url 包中聲明的這個測試函數(shù)會導致包的循環(huán)引用,但是 Go 規(guī)范禁止循環(huán)引用。
為了解決測試時可能會出現(xiàn)的循環(huán)引用的問題,可以將這個測試函數(shù)定義在外部測試包中。

聲明外部測試包

具體做法就是,測試文件的包名不和被測試的包同名,而是使用一個新的包名。在這個例子里,就是原本包名是 url,現(xiàn)在因為要導入高級別的包會出現(xiàn)循環(huán)引用,所以將包名改成一個別的名稱,比如 url_test。這個額外的后綴 _test 告訴 go test 工具,它應該單獨地編譯這個包,然后進行它的測試。為了便于理解,可以認為這個外部測試包的導入路徑是 net/url_test,但事實上它無法通過任何路徑導入。
由于外部測試在一個單獨的包里,因此它們可以引用一些依賴于被測試包的幫助包,這個是包內(nèi)測試無法做到的。從設計層次來看,外部測試包邏輯上在它所依賴的兩個包之上。
為了避免包循環(huán)導入,外部測試包允許測試用例,尤其是集成測試用例(用來測試多個組件的交互),自由地導入其他的包,就像一個引用程序那樣。

使用 go list 工具

可以使用 go list 工具來匯總一個包目錄中哪些是產(chǎn)品代碼,哪些是包內(nèi)測試、哪些是外部測試。這里用 fmt 包作為例子。

GoFiles
這類文件是包含產(chǎn)品代碼的文件列表,這些文件是 go build 命令將編譯進程序的代碼:

PS H:\Go\src\gopl\ch21> go list -f="{{.GoFiles}}" fmt
[doc.go format.go print.go scan.go]

TestGoFiles
這類文件也屬于 fmt 包,但是這些以 _test.go 結(jié)尾的文件是測試源碼文件,僅在編譯測試的時候才會使用:

PS H:\Go\src\gopl\ch21> go list -f="{{.TestGoFiles}}" fmt
[export_test.go]

這里的 export_test.go 這個文件還有特殊的意義,后面會單獨講。

XTestGoFiles
這類是包外部測試文件列表,這些同樣的測試源碼文件,僅用在測試過程中:

PS H:\Go\src\gopl\ch21> go list -f="{{.XTestGoFiles}}" fmt
[example_test.go fmt_test.go scan_test.go stringer_test.go]

白盒測試技巧

這是一個在外部測試中使用白盒測試的技巧,包內(nèi)的白盒測試沒有這個問題。
有時候,外部測試包需要對被測試包擁有特殊的訪問權限。比如這種的情況:為了避免循環(huán)引用,需要聲明外部測試包,但是又要做白盒測試,需要調(diào)用非導出的變量和函數(shù)。
應對這種情況,需要使用一種小技巧:在包內(nèi)測試文件中添加一些聲明,將包內(nèi)部的功能暴露給外部測試。由于是聲明在測試文件中的,所以暴露的后門只有在測試時可用。如果一個源文件存在的唯一目的就在于此,并且也不包含任何測試,這個文件一般就命名為 export_test.go。
下面是 fmt 包的 export_test.go 文件里所有的代碼部分:

package fmt

var IsSpace = isSpace
var Parsenum = parsenum

fmt 包的實現(xiàn)需要功能 unicode.isSpace 作為 fmt.Scanf 的一部分。為了避免創(chuàng)建不合理的依賴,fmt 沒有導入 unicode 包及其巨大的數(shù)據(jù)表,而是包含了一個更加簡單的實現(xiàn) isSpace。
為了確保 fmt.isSpace 和 unicode.isSpace 的功能一致,fmt 添加了一個測試。這是一個集成測試,所以用了外部測試包。但是測試中需要訪問 isSpace,這是一個非導出的函數(shù)。所以就有了上面的代碼,定義了一個可導出的變量來引用 isSpace 函數(shù)。并且這段代碼是定義在測試文件中的,所以無法在產(chǎn)品代碼中訪問到這個函數(shù)。

這個技巧在任何外部測試需要使用白盒測試技術的時候都可以使用。

編寫有效測試

Go 語言的測試期望測試的編寫者自己來做大部分工作,通過定義函數(shù)來避免重復。測試的過程不是死記硬背地填表格,測試也是有用戶界面的,雖然它的用戶也是它的維護者。

好的測試

一個好的測試,不會在發(fā)生錯誤時崩潰,而是要輸出一個簡潔、清晰的現(xiàn)象描述來報告錯誤,以及與之上下文相關的信息。理想情況下,不需要再通過閱讀源代碼來探究失敗的原因。
一個好的測試,不應該在發(fā)現(xiàn)一次測試失敗后就終止,而是要在一次運行中嘗試報告多個錯誤,因為錯誤發(fā)生的方式本身會揭露錯誤的原因。

舉例說明

下面的斷言函數(shù)比較兩個值,構(gòu)建一條一般的錯誤消息,并且停止程序。這是一個錯誤的例子,輸出的錯誤消息毫無用處。它的最大的問題就是沒有提供一個好的用戶界面:

import (
    "fmt"
    "strings"
    "testing"
)

// 一個糟糕的斷言函數(shù)
func assertEqual(x, y int) {
    if x != y {
        panic(fmt.Sprintf("%d != %d", x, y))
    }
}
func TestSplit(t *testing.T) {
    words := strings.Split("a:b:c", ":")
    assertEqual(len(words), 3)
    // ...
}

合適的做法
這里斷言函數(shù)犯了過早抽象的錯誤:僅僅測試兩個整數(shù)是否相同,而沒能根據(jù)上下文提供更有意義的錯誤信息。這里可以根據(jù)具體的錯誤信息提供一個更好的錯誤輸出。比如下面的做法。只有在測試中出現(xiàn)了重復的模式時才需要引入抽象:

func TestSplit(t *testing.T) {
    s, sep := "a:b:c", ":"
    words := strings.Split(s, sep)
    if got, want := len(words), 3; got != want {
        t.Errorf("Split(%q, %q) returned %d words, want %d",
            s, sep, got, want)
    }
    // ...
}

現(xiàn)在測試函數(shù)友好的用戶界面表現(xiàn)在一下幾個方面

  • 報告調(diào)用的函數(shù)名稱、它的輸入以及輸出表示的含義
  • 顯式的區(qū)分出實際值和期望值
  • 并且及時測試失敗也能夠繼續(xù)執(zhí)行。

當有了這樣的一個測試函數(shù)之后,下一步不是定義一個函數(shù)來替代整個 if 語句,而是在一個循環(huán)中執(zhí)行這個測試,就像之前基于表的測試方式那樣。
當然定義一個函數(shù)來替代整個 if 語句也是可以的做法,只是這個例子太簡單了,并不需要任何工具函數(shù)。但是為了使得測試代碼更簡潔,也可以考慮引入工具函數(shù),如果上面的 assertEqual 函數(shù)的實現(xiàn)的用戶界面更加友好的話。并且如果這種模式在其他測試代碼里也會重復用到,那就更有必要進行抽象了。
一個好的測試的關鍵是首先實現(xiàn)你所期望的具體行為,之后再使用工具函數(shù)來使代碼簡潔并且避免重復。好的結(jié)果很少是從抽象的、通用的測試函數(shù)開始的。

這里再預告一點,比較兩個變量的值在測試中很常見,并且會需要對各種類型的值進行比較,這就需要基于反射來實現(xiàn)。另外還會需要比較復合類型,這通過基于地址來判斷引用的變量是否是同一個變量來實現(xiàn),這是 unsafe 包的內(nèi)容。在掌握了反射的內(nèi)容之后,在 unsafe 包的內(nèi)容里,會實現(xiàn)一個深度相等的工具函數(shù)。

避免脆弱的測試

如果一個應用在遇到新的合法輸入的情況下經(jīng)常崩潰,那么這個程序是有缺陷的
如果在程序發(fā)生可靠的改動的時候測試用例奇怪地失敗了,那么這個測試用例也是脆弱的
避免寫出脆弱測試的最簡單的方法就是僅檢查你關心的屬性。例如,不要對輸出的字符串進行完全匹配,而是尋找到在程序進化過程中不會發(fā)生改變的子串。通常情況下,這值得寫一個穩(wěn)定的函數(shù)來從復雜的輸出中提取核心內(nèi)容,只有這樣之后的斷言才會可靠。這雖然需要一些額外的工作,但這是值得的,否則這些時間會被花在修復那些奇怪地失敗的測試上面。

覆蓋率

語句覆蓋率是一種最簡單的且廣泛使用的方法之一。一個測試套件的語句覆蓋率是指部分語句在一次執(zhí)行中執(zhí)行執(zhí)行一次??梢允褂?go cover 工具,這個工具被集成到了 go test 中,用來衡量語句覆蓋率并幫助識別測試之間的明顯差別。
如果使用VSCode,直接通過測試源碼文件里的按鈕運行測試,再切換到源碼文件中就能看到測試覆蓋率的效果。下面講的是不依賴編輯器和插件的做法。

生成覆蓋率報告

通過下面的命令可以輸出覆蓋工具的使用方法:

PS G:\Steed\Documents\Go\src\gopl\ch21\storage2> go tool cover
Usage of 'go tool cover':
Given a coverage profile produced by 'go test':
        go test -coverprofile=c.out

Open a web browser displaying annotated source code:
        go tool cover -html=c.out
...

命令 go tool 運行 Go 工具鏈里的一個可執(zhí)行文件。這些程序位于 $GOROOT/pkg/tool/${GOOS}_{GOARCH},就是 Go 安裝目錄里的文件夾下,都是一些 exe 文件。這里多虧了 go build 工具,我們不需要直接運行它。

-coverprofile 標記
要生成覆蓋率報告,需要帶上 -coverprofile 標記來運行測試:

PS G:\Steed\Documents\Go\src> go test -run=CheckQuotaNotifiesUser -coverprofile="c.out" gopl/ch21/storage2
ok      gopl/ch21/storage2      0.349s  coverage: 58.3% of statements
PS G:\Steed\Documents\Go\src>

這個標記通過檢測產(chǎn)品代碼,啟用了覆蓋數(shù)據(jù)收集。也就是說,它修改了源代碼的副本,這樣在這個語句塊執(zhí)行之前,設置一個布爾變量,每個語句塊都對應一個變量。在修改程序退出之前,它將每個變量的值都寫入到指定的日志文件,這里是 c.out,并記錄被執(zhí)行語句的匯總信息。

-cover 標記
如果不需要記錄這個日志文件而只要查看命令行輸出的內(nèi)容,可以使用 -cover 標記:

PS G:\Steed\Documents\Go\src> go test -run=CheckQuotaNotifiesUser -cover gopl/ch21/storage2
ok      gopl/ch21/storage2      0.366s  coverage: 58.3% of statements
PS G:\Steed\Documents\Go\src>

效果是一樣的,只是不生成記錄文件。

-convermode=count 標記
默認的 mode 是 set。這個標記使每個語句塊的檢測使用一個遞增計數(shù)器來替代原本的布爾值。這樣日志中就能統(tǒng)計到每個塊的執(zhí)行次數(shù),由此可以識別出執(zhí)行頻率較高的“熱塊”和相反的“冷塊”。
VSCode似乎不能指定這個模式,所以只能生成查看布爾值的報告,檢查代碼是否被覆蓋,看不到熱塊和冷塊的效果。

查看覆蓋率報告

在生成數(shù)據(jù)后,運行 cover 工具來處理生成的日志,可以生成一個 HTML 報告??梢栽跒g覽器里直觀的查看:

PS G:\Steed\Documents\Go\src> go tool cover -html="c.out"

Benchmark 函數(shù)

基準測試就是在一定的工作負載之下檢測程序性能的一種方法。
基準測試函數(shù)看上去和功能測試函數(shù)差不多,前綴是 Benchmark 并且擁有一個 *testing.B 參數(shù)。*testing.B 和 *testing.T 差不多,還額外增加了一些和性能檢測相關的方法。另外它還有一個整型成員 *testing.B.N,用來指定被檢測操作的執(zhí)行次數(shù)。

基準測試函數(shù)

回到之前的檢查回文的函數(shù),下面是 IsPalindrome 函數(shù)的基準測試,它在一個循環(huán)中調(diào)用了 IsPalindrome 共 N 次:

func BenchmarkIsPalindrome(b *testing.B) {
    for i := 0; i < b.N; i++ {
        IsPalindrome("山西懸空寺空懸西山")
    }
}

上面的基準測試函數(shù)直接加到之前的測試源碼文件中。
在基準測試函數(shù)中手動寫代碼來實現(xiàn)循環(huán),而不是在測試驅(qū)動程序中自動實現(xiàn)是有原因的。在基準測試函數(shù)中,for循環(huán)之外,可以執(zhí)行一些必要的初始化代碼并且這段時間不會加到每次迭代的時間中去。如果有代碼會干擾結(jié)果,參數(shù) testing.B 還提供了方法來停止、恢復和重置計時器(需要用到的場景并不多)。

執(zhí)行基準測試

依然是使用 go test 命令來進行測試,但是默認情況下不會運行任何基準測試。需要加上 -bench 參數(shù)并指定有運行的基準測試。它是一個匹配 Benchmark 函數(shù)名稱的正則表達式,默認值不匹配任何函數(shù)??梢允褂命c來匹配所有的基準測試函數(shù):

PS G:\Steed\Documents\Go\src\gopl\ch21\word2> go test -bench="."
goos: windows
goarch: amd64
pkg: gopl/ch21/word2
BenchmarkIsPalindrome-4          1000000              1052 ns/op
PASS
ok      gopl/ch21/word2 2.253s
PS G:\Steed\Documents\Go\src\gopl\ch21\word2>

基準測試函數(shù)名稱后面的數(shù)字后綴表示 GOMAXPROCS 的值。這對于一些并發(fā)相關的基準測試是一個重要的信息。
報告顯示每次調(diào)用 IsPalindrome 的平均耗時是 1.052ms,這個是 1000000 次調(diào)用的平均值。基準測試運行器在開始的時候并不清楚測試操作的耗時,所以開始會用比較小的N值來做檢測,然后為了檢測穩(wěn)定的運行時間,會推斷出一個較大的次數(shù)來保證得到穩(wěn)定的測試結(jié)果。

提升效率

現(xiàn)在有了基準測試,那么就先想辦法來讓程序更快一點,然后再運行基準測試來檢查具體快了多少。
有一處是明顯可以改進的,只需要遍歷字符串前面一半的字符就可以完成字符串的檢查。避免了第二次的重復比較:

    n := len(letters)
    for i := 0; i < n; i++ {
        if letters[i] != letters[len(letters)-1-i] {
            return false
        }
    }
    return true

但是通常情況下,優(yōu)化并不能總是帶來期望的好處。這個優(yōu)化后的運行時間也就 1.004ms,只有4.5%的提升。

另外還有一處可以優(yōu)化,為 letters 預分配一個容量足夠大的數(shù)組,避免在 append 調(diào)用的時候多次進行擴容:

    // var letters []rune
    letters := make([]rune, 0, len(s))
    for _, r := range s {
        if unicode.IsLetter(r) {
            letters = append(letters, unicode.ToLower(r))
        }
    }

這次改進后平均運行時間縮短到了 0.839ms,提升了20%。

查看內(nèi)存分配

如上面的例子所示,最快的程序通常是那些進行內(nèi)存分配數(shù)量最少的程序。命令行標記 -benchmem 在報告中會包含內(nèi)存分配統(tǒng)計數(shù)據(jù)。下面是優(yōu)化前后兩個函數(shù)的基準測試報告:

Running tool: D:\Go\bin\go.exe test -benchmem -run=^$ gopl\ch21\word2 -bench . -coverprofile=C:\Users\Steed\AppData\Local\Temp\vscode-gotvbvaq\go-code-cover

goos: windows
goarch: amd64
pkg: gopl/ch21/word2
BenchmarkIsPalindrome-4      1000000          1095 ns/op         120 B/op          4 allocs/op
BenchmarkIsPalindrome2-4     2000000           871 ns/op         112 B/op          1 allocs/op
PASS
coverage: 88.2% of statements
ok      gopl/ch21/word2 4.185s
Success: Benchmarks passed.

優(yōu)化前有4次內(nèi)存分配,分配了120B的內(nèi)存。優(yōu)化有只進行了1次內(nèi)存分配,分配了112B的內(nèi)存。(這里關于內(nèi)存的分配主要是切片擴容的機制。)

性能比較函數(shù)

之前的性能測試是告訴我們給定操作的絕對耗時,但是在很多情況下,需要關注的問題是兩個不同操作之間的相對耗時。比如如下的場景:

  • 如果一個函數(shù)需要1ms來處理一千個元素,那么處理一萬個或者一百萬個元素需要多久。這樣的比較能揭示漸進增長函數(shù)的運行時間
  • I/O緩沖區(qū)要設置多大最佳。對一個應用使用一系列的大小進行基準測試,可以幫助我們選擇最小的緩沖區(qū)并帶來最佳的性能表現(xiàn)
  • 對于一個任務來講,哪種算法表現(xiàn)最佳?對兩個不同的算法使用相同的輸入,在重要的或者具有代表性的工作負載下,進行基準測試通常可以顯示出每個算法的優(yōu)缺點

性能比較函數(shù)只是普通的代碼,表現(xiàn)形式通常是帶有一個參數(shù)的函數(shù),再被多個不同的 Benchmark 函數(shù)傳入不同的值來調(diào)用,比如下面這樣:

func benchmark(b *testing.B, size int) { /* ... */ }
func Benchmark10(b *testing.B)         { benchmark(b, 10) }
func Benchmark100(b *testing.B)        { benchmark(b, 100) }
func Benchmark1000(b *testing.B)       { benchmark(b, 1000) }

參數(shù) size 指定了輸入的大小,每個 Benchmark 函數(shù)傳入的值都不同但是在每個函數(shù)內(nèi)部是一個常量。不要使用 b.N 來控制輸入的大小。除非是把它當做固定大小輸入的循環(huán)次數(shù),否則基準測試的結(jié)果將毫無意義。
基準測試比較揭示的模式在程序設計階段很有用處,但是即使程序正常工作了,也不要丟掉基準測試。隨著程序的演變,或者它的輸入增長了,或者它被部署在其他的操作系統(tǒng)上并擁有一些新特性,這時仍然可以重用基準測試來回顧當初的設計決策。

性能剖析

當希望仔細地查看程序的速度是,發(fā)現(xiàn)關鍵代碼的最佳技術就是性能剖析。性能剖析是通過自動化手段在程序執(zhí)行過程中基于一些性能事件的采樣來進行性能評測,然后再從這些采樣中推斷分析,得到的統(tǒng)計報告就稱作為性能剖析(profile)。

獲取報告

Go 支持很多種性能剖析方式。其中,工具 go test 內(nèi)置支持一些類別的性能剖析:

  • CPU 性能剖析
  • 堆性能剖析
  • 阻塞性能剖析

CPU 性能剖析
CPU 性能剖析識別出執(zhí)行過程中需要 CPU 最多的函數(shù)。在每個 CPU 上面執(zhí)行的線程都每隔幾毫秒會定期地被操作系統(tǒng)中斷,在每次中斷過程中記錄一個性能剖析事件,然后恢復正常執(zhí)行。

堆性能剖析
堆性能剖析識別出負責分配最多內(nèi)存的語句。性能剖析庫對協(xié)程內(nèi)部內(nèi)存分配調(diào)用進行采樣,平均每 512KB 的內(nèi)存申請會觸發(fā)一個性能剖析事件。

阻塞性能剖析
阻塞性能剖析識別出那些阻塞協(xié)程最久的操作,例如系統(tǒng)調(diào)用,通道發(fā)送和接收數(shù)據(jù),以及鎖等待等。性能分析庫在一個 goroutine 每次被上述操作之一阻塞的時候記錄一個事件。

獲取性能剖析報告很容易,只需要像下面這樣指定一個標志參數(shù)即可。一次只獲取一種性能剖析報告,如果使用了多個標志,一種類別的報告會把其他類別的報告覆蓋掉:

$ go test -cpuprofile=cpu.out
$ go test -blockprofile=block.out
$ go test -memprofile=mem.out

還可以對非測試程序進行性能剖析,性能剖析對于長時間運行的程序尤其有用。所以 Go 運行時的性能剖析特性可以通過 runtime API 來啟用。

分析報告

在獲取了性能剖析報告后,需要使用 pprof 工具來分析它。這是 Go 自帶的一個工具,但是因為不經(jīng)常使用,所以通過 go tool pprof 間接來使用它。它有很多特性和選項,但是基本的用法只有兩個參數(shù):

  • 產(chǎn)生性能剖析結(jié)果的可執(zhí)行文件
  • 性能剖析日志

為了使得性能剖析過程高效并且節(jié)約空間,性能剖析日志里沒有包含函數(shù)名稱而是使用它們的地址。這就需要可執(zhí)行文件才能理解理解數(shù)據(jù)內(nèi)容。通常情況下 go test 工具在測試完成之后就丟棄了用于測試而臨時產(chǎn)生的可執(zhí)行文件,但在性能剖析啟用的時候,它保存并把可執(zhí)行文件命名為 foo.test,其中 foo 是被測試包的名字。

示例

下面的命令演示如何獲取和顯示簡單的 CPU 性能剖析。這里選擇了 net\/http 包中的一個基準測試。通常情況下最后對我們關心的具有代表性的具體負載而構(gòu)建的基準測試進行性能剖析。對測試用例進行基準測試永遠沒有代表性,這里使用了過濾器 -run=NONE 來禁止那些測試:

F:\>go test -run=NONE -bench=ClientServerParallelTLS64 -cpuprofile=cpu.log net/http
goos: windows
goarch: amd64
pkg: net/http
BenchmarkClientServerParallelTLS64-4    2019/04/24 15:40:39 http: TLS handshake error from 127.0.0.1:55188: read tcp 127.0.0.1:55163->127.0.0.1:55188: use of closed network connection
2019/04/24 15:40:39 http: TLS handshake error from 127.0.0.1:55366: read tcp 127.0.0.1:55264->127.0.0.1:55366: use of closed network connection
2019/04/24 15:40:41 http: TLS handshake error from 127.0.0.1:57477: read tcp 127.0.0.1:57266->127.0.0.1:57477: use of closed network connection
   10000            198886 ns/op            9578 B/op        107 allocs/op
PASS
ok      net/http        3.697s

F:\>

運行完上面的測試后,會生成兩個文件,一個是測試報告,一個是用于測試而臨時產(chǎn)生的可執(zhí)行文件。再用下面的命令打印測試報告:

F:\>go tool pprof -text -nodecount=10 ./http.test cpu.log
./http.test: open ./http.test: The system cannot find the file specified.
Fetched 1 source profiles out of 2
Type: cpu
Time: Apr 24, 2019 at 3:40pm (CST)
Duration: 2.71s, Total samples = 9820ms (362.69%)
Showing nodes accounting for 5720ms, 58.25% of 9820ms total
Dropped 370 nodes (cum <= 49.10ms)
Showing top 10 nodes out of 217
      flat  flat%   sum%        cum   cum%
    4220ms 42.97% 42.97%     4270ms 43.48%  runtime.cgocall
     210ms  2.14% 45.11%      260ms  2.65%  runtime.step
     200ms  2.04% 47.15%      490ms  4.99%  runtime.pcvalue
     190ms  1.93% 49.08%      190ms  1.93%  math/big.addMulVVW
     180ms  1.83% 50.92%      180ms  1.83%  runtime.osyield
     160ms  1.63% 52.55%      320ms  3.26%  runtime.scanobject
     160ms  1.63% 54.18%      160ms  1.63%  vendor/golang_org/x/crypto/curve25519.ladderstep
     150ms  1.53% 55.70%      150ms  1.53%  runtime.findObject
     140ms  1.43% 57.13%      140ms  1.43%  runtime.memmove
     110ms  1.12% 58.25%     1020ms 10.39%  runtime.gentraceback

F:\>

標記 -text 指定輸出的格式,這里用的是一個文本表格,表格中每行是一個函數(shù),這些函數(shù)是根據(jù)消耗CPU最多的規(guī)則排序的“熱函數(shù)”。
標記 -nodecount=10 限制輸出最高的10條記錄。

這里是一份書上的性能剖析結(jié)果:

$ go tool pprof -text -nodecount=10 ./http.test cpu.log
2570ms of 3590ms total (71.59%)
Dropped 129 nodes (cum <= 17.95ms)
Showing top 10 nodes out of 166 (cum >= 60ms)
    flat  flat%   sum%     cum   cum%
  1730ms 48.19% 48.19%  1750ms 48.75%  crypto/elliptic.p256ReduceDegree
   230ms  6.41% 54.60%   250ms  6.96%  crypto/elliptic.p256Diff
   120ms  3.34% 57.94%   120ms  3.34%  math/big.addMulVVW
   110ms  3.06% 61.00%   110ms  3.06%  syscall.Syscall
    90ms  2.51% 63.51%  1130ms 31.48%  crypto/elliptic.p256Square
    70ms  1.95% 65.46%   120ms  3.34%  runtime.scanobject
    60ms  1.67% 67.13%   830ms 23.12%  crypto/elliptic.p256Mul
    60ms  1.67% 68.80%   190ms  5.29%  math/big.nat.montgomery
    50ms  1.39% 70.19%    50ms  1.39%  crypto/elliptic.p256ReduceCarry
    50ms  1.39% 71.59%    60ms  1.67%  crypto/elliptic.p256Sum

這個性能剖析結(jié)果告訴我們,HTTPS基準測試中 crypto\/elliptic.p256ReduceDegree 函數(shù)占用了將近一半的CPU資源,對性能占很大比重。
相比之下,上面的性能剖析結(jié)果中,主要是runtime包的內(nèi)存分配的函數(shù),那么減少內(nèi)存消耗是一個有價值的優(yōu)化。

對于更微妙的問題,最好使用 pprof 的圖形顯示功能。這需要 GraphViz 工具,可以從 http://www.graphviz.org 下載。然后使用標記 -web 生成函數(shù)的有向圖,并能標記出函數(shù)的CPU消耗數(shù)值,以及有顏色突出“熱函數(shù)”。點到為止,未展開。

Example 函數(shù)

這是第三種也是最后一種測試函數(shù),示例函數(shù)。名字以 Example 開頭,既沒有參數(shù),也沒有返回值。
下面是IsPalindrome函數(shù)對應的示例函數(shù):

func ExampleIsPalindrome() {
    fmt.Println(IsPalindrome("A man, a plan, a canal: Panama"))
    fmt.Println(IsPalindrome("palindrome"))
    // Output:
    // true
    // false
}

示例函數(shù)有三個目的:

  • 用作文檔
  • 作為可執(zhí)行測試
  • 提供一個真實的演練場

用作文檔

比起乏味的描述,舉一個好的例子是描述庫函數(shù)功能最簡潔直觀的方式。
基于 Example 函數(shù)的后綴,基于 Web 的文檔服務器 godoc 可以將示例函數(shù)(比如:ExampleIsPalindrome)和它所演示的函數(shù)或包(比如:IsPalindrome函數(shù)),關聯(lián)起來。
如果是一個名字叫 Example 的函數(shù),那么就會和包的文檔關聯(lián)。

作為可執(zhí)行測試

示例函數(shù)是可以通過 go test 運行的可執(zhí)行測試。示例函數(shù)的最后如果有一段類型 // Output: 的注釋,就像上面的例子里一樣。測試驅(qū)動程序?qū)?zhí)行這個函數(shù)并且檢查輸出到終端的內(nèi)容與注釋是否匹配。

提供一個真實的演練場

http://golang.org 就是由 godoc 提供的文檔服務,它使用 Go Playground 來讓用戶在 Web 瀏覽器上編輯和運行每個示例函數(shù)。這可以作為了解特定函數(shù)功能或者了解語言特性最快捷的方法。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI