欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

在Go中編寫測試代碼的方法總結(jié)

 更新時(shí)間:2023年07月17日 10:14:31   作者:江湖十年  
在程序開發(fā)過程中,測試是非常重要的一環(huán),甚至有一種開發(fā)模式叫?TDD,先編寫測試,再編寫功能代碼,通過測試來推動整個(gè)開發(fā)的進(jìn)行,可見測試在開發(fā)中的重要程度,為此,Go提供了testing框架來方便我們編寫測試,本文將向大家介紹在Go中如何編寫測試代碼

測試分類

在 Go 中,編寫的測試用例可以分為四類:

  • 單元測試:測試函數(shù)名稱以 Test 開頭,如 TestXxxTest_Xxx,用來對程序的最小單元進(jìn)行測試,如函數(shù)、方法等。

  • 基準(zhǔn)測試:也叫性能測試,測試函數(shù)名稱以 Benchmark 開頭,用來測量程序的性能指標(biāo)。

  • 示例測試:測試函數(shù)名稱以 Example 開頭,可以用來測試程序的標(biāo)準(zhǔn)輸出內(nèi)容。

  • 模糊測試:也叫隨機(jī)測試,測試函數(shù)名稱以 Fuzz 開頭,是一種基于隨機(jī)輸入的自動化測試技術(shù),適合用來測試處理用戶輸入的代碼,在 Go 1.18 中被引入。

這四類測試用例都有各自的適用場景,其中單元測試最為常用,你一定要掌握。

以上這些測試用例都可以使用 go test 命令來執(zhí)行。

測試規(guī)范

編寫測試代碼并不需要我們學(xué)習(xí)新的 Go 語法,但有些測試規(guī)范還是需要遵守的。

Go 在提供 testing 測試框架時(shí),就規(guī)定了很多測試規(guī)范,用來約束我們編寫測試的方式,這有助于項(xiàng)目的工程化。

測試文件命名規(guī)范

首先,測試文件命名必須以 _test.go 結(jié)尾,否則將被測試框架忽略。比如我們的 Go 代碼文件名為 hello.go,則測試文件可以命名為 hello_test.go。

只有以 _test.go 結(jié)尾的測試文件,才能使用 go test 命令執(zhí)行。

在構(gòu)建 Go 程序時(shí),go build 命令會忽略以 _test.go 結(jié)尾的測試文件。

測試包命名規(guī)范

測試用例除了根據(jù)使用場景可以分為四類,還可以根據(jù)代碼和測試用例是否在一個(gè)包中,分為白盒測試和黑盒測試。

  • 白盒測試:將測試代碼和被測代碼放在同一個(gè)包中,也就是二者包名相同,這些測試用例屬于白盒測試。比如 Go 代碼文件名為 hello.go,包名為 hello,測試文件可以命名為 hello_test.go,并且必須與 hello.go 放在同一個(gè)目錄下,包名也必須為 hello。白盒測試的測試用例可以使用和測試當(dāng)前包中所有標(biāo)識符(變量、函數(shù)等),包括未導(dǎo)出的標(biāo)識符。

  • 黑盒測試:將測試代碼和被測代碼放在不同的包中,即包名不同,這些測試用例屬于黑盒測試。比如 Go 代碼文件名為 hello.go,包名為 hello,測試文件同樣可以命名為 hello_test.go,與 hello.go 放在同一個(gè)目錄下,但包名不能再叫 hello,應(yīng)該命名為 hello_testhello_test.go 文件也可以放在專門的 test 目錄下,此時(shí)可以隨意命名包名。黑盒測試的測試用例僅能夠使用和測試被測代碼包中可導(dǎo)出的標(biāo)識符,因?yàn)槎咭呀?jīng)不再屬于同一個(gè)包,這遵循 Go 語法規(guī)范。

根據(jù)二者各自特點(diǎn),在開發(fā)時(shí)我們應(yīng)該多編寫白盒測試,這樣才能提升代碼測試覆蓋率。

測試用例命名規(guī)范

在 Go 中我們使用測試函數(shù)來編寫測試用例,根據(jù)單元測試、基準(zhǔn)測試、示例測試、模糊測試四種不同類型的測試分類,測試函數(shù)必須以 Test、Benchmark、Example、Fuzz 其中一種開頭。

測試函數(shù)簽名示例如下:

func TestXxx(*testing.T)
func BenchmarkXxx(*testing.B)
func ExampleXxx()
func FuzzXxx(*testing.F)

測試函數(shù)不能有返回值,其中單元測試、基準(zhǔn)測試和模糊測試都接收一個(gè)參數(shù),由 testing 框架提供,示例測試則不需要傳遞參數(shù)。

其中 Xxx 一般是被測試的函數(shù)名稱,首字母必須大寫。如果是以 Test_Xxx 方式命名測試函數(shù),則 Xxx 首字母大小寫均可。

測試變量命名規(guī)范

對于測試變量的命名,testing 框架沒有有強(qiáng)制約束,但社區(qū)中也形成了一些規(guī)范。

比如,函數(shù)簽名中的參數(shù)變量定義如下:

func TestXxx(t *testing.T)
func BenchmarkXxx(b *testing.B)
func FuzzXxx(f *testing.F)

單元測試、基準(zhǔn)測試和模糊測試參數(shù)變量即為參數(shù)類型 *testing.<T> 的小寫形式。

在編寫測試代碼時(shí),有一個(gè)最常見的場景,就是比較被測函數(shù)的實(shí)際輸出和測試函數(shù)中的預(yù)期輸出是否相等,通常可以使用 got/wantactual/expected 來命名變量:

if got != want {
	t.Errorf("Xxx(x) = %s; want %s", got, want)
}

或:

if actual != expected {
	t.Errorf("Xxx(x) = %s; expected %s", actual, expected)
}

讀者可以根據(jù)喜好和團(tuán)隊(duì)中的開發(fā)規(guī)范選擇其中一種變量命名。

此外,在單元測試中我們還會經(jīng)常編寫一種叫表格測試的測試用例,寫法如下:

func TestXxx(t *testing.T) {
	tests := []struct {
		name string
		arg  float64
		want float64
	}{
	...
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := Xxx(tt.arg); got != tt.want {
				t.Errorf("Xxx(%f) = %v, want %v", tt.arg, got, tt.want)
			}
		})
	}
}

其中 tests 代表多個(gè)測試用例,循環(huán)時(shí)以 tt 作為循環(huán)變量(tt 可以避免與單元測試函數(shù)的參數(shù)變量 t 命名沖突)。

表格測試還有另一個(gè)版本:

func TestXxx(t *testing.T) {
	cases := []struct {
		name string
		arg  float64
		want float64
	}{
	...
	}
	for _, cc := range cases {
		t.Run(cc.name, func(t *testing.T) {
			if got := Xxx(cc.arg); got != cc.want {
				t.Errorf("Xxx(%f) = %v, want %v", cc.arg, got, cc.want)
			}
		})
	}
}

現(xiàn)在 cases 代表多個(gè)測試用例,循環(huán)時(shí)以 cc 作為循環(huán)變量(cc 可以避免與常見的 context 縮寫 c 命名沖突)。

編寫測試代碼的常見規(guī)范我們就先講解到這里,更多規(guī)范將在下文講解對應(yīng)示例時(shí)再進(jìn)行詳細(xì)說明。

單元測試

單元測試是我們最常編寫的測試用例,所以先來學(xué)習(xí)下如何編寫單元測試。

首先,我們準(zhǔn)備一個(gè) Abs 函數(shù)作為被測試的代碼,存放于 abs.go 文件中,其包名為 abs,代碼如下:

package abs
import "math"
func Abs(x float64) float64 {
	return math.Abs(x)
}

白盒測試

現(xiàn)在為 Abs 編寫一個(gè)白盒測試函數(shù),在存放 abs.go 文件的同一目錄下,新建 abs_test.go 文件,包名同樣定義為 abs,編寫測試代碼如下:

package abs
import "testing"
func TestAbs(t *testing.T) {
	got := Abs(-1)
	if got != 1 {
		t.Errorf("Abs(-1) = %f; want 1", got)
	}
}

單元測試函數(shù) TestAbs 代碼非常簡單,先調(diào)用了 Abs(-1) 函數(shù),并將得到的返回結(jié)果 got1 做相等性比較,如果不相等,則說明測試沒有通過,使用 t.Errorf 打印錯(cuò)誤信息。

參數(shù) *testing.T 是一個(gè)結(jié)構(gòu)體指針,提供了如下幾個(gè)方法用于錯(cuò)誤報(bào)告:

  • t.Log/t.Logf:打印正常日志信息,類似 fmt.Print。

  • t.Error/t.Errorf:打印測試失敗時(shí)的錯(cuò)誤信息,不影響當(dāng)前測試函數(shù)內(nèi)后續(xù)代碼的繼續(xù)執(zhí)行。

  • t.Fatal/t.Fatalf:打印測試失敗時(shí)的錯(cuò)誤信息,并終止當(dāng)前測試函數(shù)執(zhí)行。

在測試函數(shù)所在目錄下使用 go test 命令執(zhí)行測試代碼:

$ go test
PASS
ok      github.com/jianghushinian/blog-go-example/test/getting-started/abs      0.139s

go test 會自動查找當(dāng)前目錄下所有以 _test.go 結(jié)尾來命名的測試文件,并執(zhí)行其內(nèi)部編寫的全部測試函數(shù)。

輸出 PASS 表示測試通過,github.com/jianghushinian/blog-go-example/test/getting-started/abs 是程序的 module 名稱。

go test 命令還支持使用 -v 標(biāo)志輸出更多信息:

$ go test -v
=== RUN   TestAbs
--- PASS: TestAbs (0.00s)
PASS
ok      github.com/jianghushinian/blog-go-example/test/getting-started/abs      0.437s

如果我們不小心將單元測試的函數(shù)名錯(cuò)誤的寫成 Testabs,即 abs 沒有大寫開頭:

func Testabs(t *testing.T) {
	got := Abs(-1)
	if got != 1 {
		t.Errorf("Abs(-1) = %f; want 1", got)
	}
}

則測試函數(shù)不會被 go test 命令執(zhí)行。

通常情況下,我們不會對一個(gè)函數(shù)只做一種輸入?yún)?shù)的測試,為了提高測試覆蓋率,我們可能還需要多測試幾種參數(shù)的用例,比如測試下 Abs(2)Abs(3) 等是否正確。

這時(shí),可以像如下這樣編寫測試函數(shù):

func TestAbs(t *testing.T) {
	got := Abs(-1)
	if got != 1 {
		t.Errorf("Abs(-1) = %f; want 1", got)
	}
	got = Abs(2)
	if got != 2 {
		t.Errorf("Abs(2) = %f; want 2", got)
	}
}

但這樣的代碼顯然過于“平鋪直敘”,不夠優(yōu)雅。

在這種更加復(fù)雜的情況下,我們可以使用「表格測試」,代碼如下:

func TestAbs_TableDriven(t *testing.T) {
	tests := []struct {
		name string
		x    float64
		want float64
	}{
		{
			name: "positive",
			x:    2,
			want: 2,
		},
		{
			name: "negative",
			x:    -3,
			want: 3,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := Abs(tt.x); got != tt.want {
				t.Errorf("Abs(%f) = %v, want %v", tt.x, got, tt.want)
			}
		})
	}
}

為了便于與之前編寫的測試函數(shù) TestAbs 區(qū)分,我為當(dāng)前測試函數(shù)命名為 TestAbs_TableDriven,代表這是一個(gè)表格驅(qū)動的測試。

在測試函數(shù)內(nèi)部,首先定義了一個(gè)匿名結(jié)構(gòu)體切片,用來保存多個(gè)測試用例。

name 是一個(gè)字符串,可以是任何句子,用來標(biāo)記當(dāng)前測試用例所測試的場景,這樣代碼維護(hù)者通過 name 字段就能夠知道當(dāng)前用例所測試的場景,作用相當(dāng)于代碼注釋。

x 作為 Abs 函數(shù)的入?yún)?,其類型等同?Abs 函數(shù)的參數(shù),如果被測試函數(shù)有多個(gè)參數(shù),這里也可以使用一個(gè)結(jié)構(gòu)體來保存。

want 記錄當(dāng)前測試用例的期望值。

for 循環(huán)中,我們可以使用 *testing.T 提供的 t.Run 方法執(zhí)行測試用例,這和直接編寫的 TestXxx 測試函數(shù)沒什么本質(zhì)區(qū)別。

現(xiàn)在使用 go test 命令執(zhí)行測試代碼:

go test -v
=== RUN   TestAbs
--- PASS: TestAbs (0.00s)
=== RUN   TestAbs_TableDriven
=== RUN   TestAbs_TableDriven/positive
=== RUN   TestAbs_TableDriven/negative
--- PASS: TestAbs_TableDriven (0.00s)
    --- PASS: TestAbs_TableDriven/positive (0.00s)
    --- PASS: TestAbs_TableDriven/negative (0.00s)
PASS
ok      github.com/jianghushinian/blog-go-example/test/getting-started/abs      0.145s

可以發(fā)現(xiàn),表格測試的輸出信息更加豐富,能夠分別打印出表格中的每一個(gè)測試用例,并且使用縮進(jìn)來展示層級關(guān)系。

現(xiàn)在我們故意將其中的一個(gè)測試用例改錯(cuò):

{
	name: "negative",
	x:    -3,
	want: 33,
}

再次使用 go test 命令執(zhí)行測試代碼看下如何輸出:

go test -v
=== RUN   TestAbs
--- PASS: TestAbs (0.00s)
=== RUN   TestAbs_TableDriven
=== RUN   TestAbs_TableDriven/positive
=== RUN   TestAbs_TableDriven/negative
    abs_test.go:36: Abs(-3.000000) = 3, want 33
--- FAIL: TestAbs_TableDriven (0.00s)
    --- PASS: TestAbs_TableDriven/positive (0.00s)
    --- FAIL: TestAbs_TableDriven/negative (0.00s)
FAIL
exit status 1
FAIL    github.com/jianghushinian/blog-go-example/test/getting-started/abs      0.515s

根據(jù)打印結(jié)果,我們很容易能夠發(fā)現(xiàn)是 TestAbs_TableDriven 測試函數(shù)中 negative 這個(gè)測試用例執(zhí)行失敗了。

有些場景下,我們可能想要跳過某些測試用例,可以使用 (*testing.T).Skip 方法來實(shí)現(xiàn):

func TestAbs_Skip(t *testing.T) {
	// CI 環(huán)境跳過當(dāng)前測試
	if os.Getenv("CI") != "" {
		t.Skip("it's too slow, skip when running in CI")
	}
	t.Log(t.Skipped())
	got := Abs(-2)
	if got != 2 {
		t.Errorf("Abs(-2) = %f; want 2", got)
	}
}

假如 TestAbs_Skip 是一個(gè)非常耗時(shí)的測試用例,我們就可以使用 t.Skip 在 CI 環(huán)境下跳過此測試。

t.Skipped() 返回當(dāng)前測試用例是否被跳過。

使用 go test 命令執(zhí)行測試:

$ CI=1 go test -v -run="TestAbs_Skip"
=== RUN   TestAbs_Skip
    abs_test.go:46: it's too slow, skip when running in CI
--- SKIP: TestAbs_Skip (0.00s)
PASS
ok      github.com/jianghushinian/blog-go-example/test/getting-started/abs      0.103s

這次我們使用 -run 參數(shù)來指定想要執(zhí)行的測試用例,-run 參數(shù)的值支持正則。

并且指定了環(huán)境變量 CI=1。

從打印結(jié)果來看,TestAbs_Skip 測試用例的確被跳過了,所以 t.Log(t.Skipped()) 沒有被執(zhí)行到。

默認(rèn)情況下,測試用例是從上到下按照順序執(zhí)行的,不過,我們可以使用 (*testint.T).Parallel 來標(biāo)記一個(gè)測試函數(shù)支持并發(fā)執(zhí)行:

func TestAbs_Parallel(t *testing.T) {
	t.Log("Parallel before")
	// 標(biāo)記當(dāng)前測試支持并行
	t.Parallel()
	t.Log("Parallel after")
	got := Abs(2)
	if got != 2 {
		t.Errorf("Abs(2) = %f; want 2", got)
	}
}

只有一個(gè)測試函數(shù)支持并發(fā)執(zhí)行意義不大,我們可以將 TestAbs 測試函數(shù)也修改為支持并發(fā)執(zhí)行:

func TestAbs(t *testing.T) {
	t.Parallel()
	got := Abs(-1)
	if got != 1 {
		t.Errorf("Abs(-1) = %f; want 1", got)
	}
}

現(xiàn)在,使用 go test 命令來測試下并發(fā)執(zhí)行測試用例:

$ go test -v -run=".*Parallel.*|^TestAbs$"
=== RUN   TestAbs
=== PAUSE TestAbs
=== RUN   TestAbs_Parallel
    abs_test.go:59: Parallel before
=== PAUSE TestAbs_Parallel
=== CONT  TestAbs
--- PASS: TestAbs (0.00s)
=== CONT  TestAbs_Parallel
    abs_test.go:62: Parallel after
--- PASS: TestAbs_Parallel (0.00s)
PASS
ok      github.com/jianghushinian/blog-go-example/test/getting-started/abs      0.200s

這里我們只執(zhí)行了 TestAbs_Parallel、TestAbs 這兩個(gè)測試函數(shù)。

可以發(fā)現(xiàn),兩個(gè)函數(shù)都不是一次性執(zhí)行完成的,日志中 PAUSE 表示暫停當(dāng)前函數(shù)的執(zhí)行,CONT 表示恢復(fù)當(dāng)前函數(shù)執(zhí)行。

有時(shí)候,我們測試的并不是一個(gè)函數(shù),而是一個(gè)方法,比如我們想要測試 Animal 結(jié)構(gòu)體的 shout 方法:

package animal
type Animal struct {
	Name string
}
func (a Animal) shout() string {
	if a.Name == "dog" {
		return "旺!"
	}
	if a.Name == "cat" {
		return "喵~"
	}
	return "吼~"
}

那么,測試函數(shù)可以命名為 TestAnimal_shout,如下是我們針對 Dog 和 Cat 兩種不同的 Animal 對象編寫的測試代碼:

package animal
import (
	"testing"
)
func TestAnimalDog_shout(t *testing.T) {
	dog := Animal{Name: "dog"}
	got := dog.shout()
	want := "旺!"
	if got != want {
		t.Errorf("got %s; want %s", got, want)
	}
}
func TestAnimalCat_shout(t *testing.T) {
	cat := Animal{Name: "cat"}
	got := cat.shout()
	want := "喵~"
	if got != want {
		t.Errorf("got %s; want %s", got, want)
	}
}

黑盒測試

講完了白盒測試,我們再來演示下如何編寫黑盒測試。

要為 Abs 編寫黑盒測試非常簡單,我們只需要將 TestAbs 移動到新的包中即可。

package abs_test
import (
	"testing"
	"github.com/jianghushinian/blog-go-example/test/getting-started/abs"
)
func TestAbs(t *testing.T) {
	got := abs.Abs(-1)
	if got != 1 {
		t.Errorf("Abs(-1) = %f; want 1", got)
	}
}

因?yàn)楹诤袦y試的函數(shù) TestAbsAbs 不在同一個(gè)包中,所以需要先使用 import 導(dǎo)入 abs 包,之后才能使用 abs.Abs 函數(shù)。

至此,常見的單元測試場景我們就介紹完了。

接下來,我們一起來看如何編寫基準(zhǔn)測試。

基準(zhǔn)測試

基準(zhǔn)測試也叫性能測試,顧名思義,是為了度量程序的性能。

Abs 編寫的基準(zhǔn)測試代碼如下:

func BenchmarkAbs(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Abs(-1)
	}
}

基準(zhǔn)測試同樣放在 abs_test.go 文件中,以 Benchmark 開頭,參數(shù)不再是 *testing.T,而是 *testing.B,在測試函數(shù)中,我們循環(huán)了 b.N 次調(diào)用 Abs(-1)b.N 的值是一個(gè)動態(tài)值,我們無需操心,testing 框架會為其分配合理的值,以使測試函數(shù)運(yùn)行足夠多的次數(shù),可以準(zhǔn)確的計(jì)時(shí)。

默認(rèn)情況下,go test 命令并不會運(yùn)行基準(zhǔn)測試,需要指定 -bench 參數(shù):

$ go test -bench="." 
goos: darwin
goarch: arm64
pkg: github.com/jianghushinian/blog-go-example/test/getting-started/abs
BenchmarkAbs-8          1000000000               0.5096 ns/op
PASS
ok      github.com/jianghushinian/blog-go-example/test/getting-started/abs      0.674s

-bench 參數(shù)同樣接收一個(gè)正則,. 匹配所有基準(zhǔn)測試。

我們需要重點(diǎn)關(guān)注的是這行結(jié)果:

BenchmarkAbs-8          1000000000               0.5096 ns/op

BenchmarkAbs-8 中,BenchmarkAbs 是測試函數(shù)名,8GOMAXPROCS 的值,即參與執(zhí)行的 CPU 核心數(shù)。

1000000000 表示測試執(zhí)行了這么多次。

0.5096 ns/op 表示每次循環(huán)平均消耗的納秒數(shù)。

如果還想查看基準(zhǔn)測試的內(nèi)存統(tǒng)計(jì)情況,則可以指定 -benchmem 參數(shù):

$ go test -bench="BenchmarkAbs$" -benchmem           
goos: darwin
goarch: arm64
pkg: github.com/jianghushinian/blog-go-example/test/getting-started/abs
BenchmarkAbs-8          1000000000               0.5097 ns/op          0 B/op          0 allocs/op
PASS
ok      github.com/jianghushinian/blog-go-example/test/getting-started/abs      0.681s

現(xiàn)在,BenchmarkAbs-8 這行得到了更多輸出:

BenchmarkAbs-8          1000000000               0.5097 ns/op          0 B/op          0 allocs/op

0 B/op 表示每次執(zhí)行測試代碼分配了多少字節(jié)內(nèi)存。

0 allocs/op 表示每次執(zhí)行測試代碼分配了多少次內(nèi)存。

此外,在執(zhí)行 go test 命令時(shí),我們可以使用 -benchtime=Ns 參數(shù)指定基準(zhǔn)測試函數(shù)執(zhí)行時(shí)間為 N 秒:

$ go test -bench="BenchmarkAbs$" -benchtime=0.1s 
goos: darwin
goarch: arm64
pkg: github.com/jianghushinian/blog-go-example/test/getting-started/abs
BenchmarkAbs-8          210435709                0.5096 ns/op
PASS
ok      github.com/jianghushinian/blog-go-example/test/getting-started/abs      0.600s

-benchtime 參數(shù)值為 time.Duration 類型支持的時(shí)間格式。

-benchtime 參數(shù)還有一個(gè)特殊語法 -benchtime=Nx 參數(shù),可以指定基準(zhǔn)測試函數(shù)執(zhí)行次數(shù)為 N 次:

$ go test -bench="BenchmarkAbs$" -benchtime=10x
goos: darwin
goarch: arm64
pkg: github.com/jianghushinian/blog-go-example/test/getting-started/abs
BenchmarkAbs-8                10                20.90 ns/op
PASS
ok      github.com/jianghushinian/blog-go-example/test/getting-started/abs      0.391s

有時(shí)候,我們在編寫基準(zhǔn)測試時(shí),被測函數(shù)可能需要一些準(zhǔn)備數(shù)據(jù),而這些準(zhǔn)備數(shù)據(jù)的時(shí)間不應(yīng)該算做被測試函數(shù)的耗時(shí)。

此時(shí),可以使用 (*testing.B).ResetTimer 重置計(jì)時(shí):

func BenchmarkAbsResetTimer(b *testing.B) {
	time.Sleep(100 * time.Millisecond) // 模擬耗時(shí)的準(zhǔn)備工作
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		Abs(-1)
	}
}

這樣,在調(diào)用 b.ResetTimer() 之前的耗時(shí)操作將不被記入測試結(jié)果的耗時(shí)中。

還有一種方法,也可以跳過準(zhǔn)備工作的計(jì)時(shí),即先使用 (*testing.B).StopTimer 停止計(jì)時(shí),耗時(shí)的準(zhǔn)備工作完成后再使用 (*testing.B).StartTimer 恢復(fù)計(jì)時(shí):

func BenchmarkAbsStopTimerStartTimer(b *testing.B) {
	b.StopTimer()
	time.Sleep(100 * time.Millisecond) // 模擬耗時(shí)的準(zhǔn)備工作
	b.StartTimer()
	for i := 0; i < b.N; i++ {
		Abs(-1)
	}
}

默認(rèn)情況下,基準(zhǔn)測試 for 循環(huán)中的代碼是串行執(zhí)行的,如果想要并行執(zhí)行,可以將被測試代碼的調(diào)用放在 (*testing.B).RunParallel 中:

func BenchmarkAbsParallel(b *testing.B) {
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			Abs(-1)
		}
	})
}

我們還可以使用 (*testing.B).SetParallelism 控制并發(fā)協(xié)程數(shù):

func BenchmarkAbsParallel(b *testing.B) {
	b.SetParallelism(2) // 設(shè)置并發(fā) Goroutines 數(shù)量為 2 * GOMAXPROCS
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			Abs(-1)
		}
	})
}

在使用 go test 命令執(zhí)行基準(zhǔn)測試時(shí),可以指定 -cpu 參數(shù)來設(shè)置 GOMAXPROCS

要想了解 go test 支持的更多參數(shù),可以使用 go help testflag 命令進(jìn)行查看。

示例測試

示例測試以 Example 開頭,無參數(shù)和返回值,通常存放在 example_test.go 文件中。

約定一個(gè)包、函數(shù) F、類型 T、方法 M 的示例測試命名如下:

func Example() { ... } // 整個(gè)包的示例測試
func ExampleF() { ... } // 函數(shù) F 的示例測試
func ExampleT() { ... } // 類型 T 的示例測試
func ExampleT_M() { ... } // 類型 T 的 M 方法的示例測試

一個(gè)包、函數(shù)、類型、方法如果存在多個(gè)示例測試,可以通過在名稱后面附加一個(gè)不同的后綴來命名示例測試函數(shù),后綴必須以小寫字母開頭,如下:

func Example_suffix() { ... }
func ExampleF_suffix() { ... }
func ExampleT_suffix() { ... }
func ExampleT_M_suffix() { ... }

以下是一個(gè)為 Abs 函數(shù)編寫的示例測試:

func ExampleAbs() {
	fmt.Println(Abs(-1))
	fmt.Println(Abs(2))
	// Output:
	// 1
	// 2
}

示例測試函數(shù)末尾需要使用 // Output: 注釋,來標(biāo)記被測試函數(shù)的標(biāo)準(zhǔn)輸出內(nèi)容。

這里分別使用 fmt.Println(Abs(-1))、fmt.Println(Abs(2)) 調(diào)用了兩次 Abs 函數(shù),所以會得到兩個(gè)輸出。

示例測試會攔截測試過程中的標(biāo)準(zhǔn)輸出,并與 // Output: 注釋之后的內(nèi)容做對比,如果相等,則測試通過。

go test 默認(rèn)情況下,不會執(zhí)行示例測試,可以通過 -run 指定示例測試函數(shù):

$ go test -v -run "ExampleAbs$"           
=== RUN   ExampleAbs
--- PASS: ExampleAbs (0.00s)
PASS
ok      github.com/jianghushinian/blog-go-example/test/getting-started/abs      2.050s

我們還可以使用 // Unordered Output: 注釋,來標(biāo)記被測試函數(shù)的標(biāo)準(zhǔn)輸出內(nèi)容。

這將忽略被測試函數(shù)的輸出順序:

func ExampleAbs_unordered() {
	fmt.Println(Abs(2))
	fmt.Println(Abs(-1))
	// Unordered Output:
	// 1
	// 2
}

以上這個(gè)示例測試函數(shù)中,無論是先調(diào)用 Abs(2) 還是先調(diào)用 Abs(-1),測試函數(shù)都能通過。

沒有輸出注釋 // Output:// Unordered Output:的示例測試函數(shù)只會被編譯,但不會執(zhí)行。

此外,示例測試還有一個(gè)非常有用功能,它能被 godocpkgsite 工具所識別,將示例函數(shù)的代碼提取后作為被測試函數(shù)文檔的一部分。

注意:舊版本的 Go 自帶了 godoc 工具,能夠在本地啟動一個(gè) Web 服務(wù)器,對本地安裝的 Go 包提供文檔服務(wù),不過現(xiàn)在官方已經(jīng)不維護(hù) godoc 了,所以不再推薦使用。Go 1.15 以后雖然集成了 go doc 工具,但是無法啟動 Web 服務(wù),比較適合命令行中查看 Go 包的文檔?,F(xiàn)在,Go 官方比較推薦使用的工具是 pkgsite,能夠啟動 Web 服務(wù),并且它與 Go 在線文檔站點(diǎn)長得一樣。

這里以 pkgsite 工具為例展示下示例測試函數(shù)生成的文檔效果。

首先,安裝 pkgsite

$ go install golang.org/x/pkgsite/cmd/pkgsite@latest

然后,在 abs.go 目錄下執(zhí)行 pkgsite 即可啟動文檔服務(wù):

$ pkgsite

現(xiàn)在訪問 http://localhost:8080 即可進(jìn)入文檔服務(wù)首頁:

點(diǎn)擊模塊名 github.com/jianghushinian/blog-go-example/test/getting-started 即可找到 Abs 函數(shù)位置,在 Abs 函數(shù)下方,標(biāo)題 Example、Example (Unordered) 下就是通過示例測試生成的示例文檔:

注意:這里需要額外提及的一點(diǎn)是,我們查看文檔的本地包模塊名稱(module)應(yīng)該帶 .,也就是一般使用域名作為包名的一部分,否則啟動 pkgsite 后將會報(bào)錯(cuò),無法查看本地包的文檔。

模糊測試

模糊測試在 Go 1.18 中被引入,模糊測試(fuzz testing)又叫隨機(jī)測試,是一種基于隨機(jī)輸入的自動化測試技術(shù)。

模糊測試比較適合用于發(fā)現(xiàn)處理用戶輸入的代碼中存在的問題。

關(guān)于模糊測試的編寫方式,有一張圖廣泛流傳:

模糊測試同樣需要放在 _test.go 文件中,并且以 Fuzz 開頭,參數(shù)為 *testing.F

上圖中,f.Add(5, "hello") 是在為模糊測試提供初始的種子語料,其實(shí)就是被測試函數(shù)接收的合法參數(shù),后續(xù)的模糊測試過程中,會根據(jù)這個(gè)種子語料,生成更多的模糊測試參數(shù)。這有點(diǎn)類似我們生成隨機(jī)數(shù)時(shí)需要傳遞一個(gè)隨機(jī)種子。雖然調(diào)用 f.Add 方法不是必須的,但提供合法的種子語料有利于更早發(fā)現(xiàn)被測試函數(shù)的問題。

f.Fuzz 是模糊測試的主體邏輯,它接收一個(gè)函數(shù),函數(shù)的第一個(gè)參數(shù)為 *testing.T,之后是被測函數(shù)接收的參數(shù),稱為 Fuzzing arguments

Fuzzing arguments 參數(shù)是 testing 框架隨機(jī)生成的,所以叫隨機(jī)測試,這些隨機(jī)生成的參數(shù)將依次傳遞給 Foo 函數(shù)。

調(diào)用 Foo 函數(shù)和判斷測試結(jié)果是否正確的代碼,就跟我們編寫的普通單元測試一樣了。

可以發(fā)現(xiàn),模糊測試相較于單元測試,多了一個(gè)自動生成測試參數(shù)的過程。

不過,Fuzzing arguments 支持的參數(shù)類型有限,僅支持如下幾種類型:

  • string, []byte
  • int, int8, int16, int32/rune, int64
  • uint, uint8/byte, uint16, uint32, uint64
  • float32, float64
  • bool

此外,編寫模糊測試時(shí),Fuzz target 不要依賴全局狀態(tài),因?yàn)槟:郎y試會并行執(zhí)行。

為了演示如何編寫模糊測試,我編寫了一個(gè) Hello 函數(shù):

package hello
import "errors"
var (
	ErrEmptyName   = errors.New("empty name")
	ErrTooLongName = errors.New("too long name")
)
func Hello(name string) (string, error) {
	if name == "" {
		return "", ErrEmptyName
	}
	if len(name) > 10 {
		return "", ErrTooLongName
	}
	return "Hello " + name, nil
}

將 Hello 函數(shù)放在 hello.go 文件中。

在 Hello 函數(shù)內(nèi)部,對 name 參數(shù)進(jìn)行了校驗(yàn),不能為空,且長度不能超過 10。

為 Hello 函數(shù)編寫模糊測試代碼如下:

func FuzzHello(f *testing.F) {
	f.Add("Foo")
	f.Fuzz(func(t *testing.T, name string) {
		_, err := Hello(name)
		if err != nil {
			if errors.Is(err, ErrEmptyName) || errors.Is(err, ErrTooLongName) {
				return
			}
			t.Errorf("unexpected error: %s, name: %s", err, name)
		}
	})
}

模糊測試代碼放在 hello_fuzz_test.go 中,包名同樣為 hello。

f.Fuzz 中調(diào)用了 Hello 函數(shù),并判斷返回的 err 是否符合預(yù)期,如果不符合預(yù)期,則表示測試失敗。

go test 命令默認(rèn)情況下同樣不會執(zhí)行模糊測試,我們需要指定 -fuzz 參數(shù):

$ go test -fuzz="FuzzHello"
fuzz: elapsed: 0s, gathering baseline coverage: 0/1 completed
fuzz: elapsed: 0s, gathering baseline coverage: 1/1 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 521335 (173703/sec), new interesting: 2 (total: 3)
fuzz: elapsed: 6s, execs: 947014 (141945/sec), new interesting: 2 (total: 3)
fuzz: elapsed: 9s, execs: 1391822 (148228/sec), new interesting: 2 (total: 3)
fuzz: elapsed: 12s, execs: 1838008 (148764/sec), new interesting: 2 (total: 3)
fuzz: elapsed: 15s, execs: 2266978 (143002/sec), new interesting: 2 (total: 3)
^Cfuzz: elapsed: 15s, execs: 2308214 (139431/sec), new interesting: 2 (total: 3)
PASS
ok      github.com/jianghushinian/blog-go-example/test/getting-started/hello    17.131s

以上測試執(zhí)行過程中,我使用 ^C 終止了測試。模糊測試默認(rèn)情況下會一直執(zhí)行下去,直至遇到 crash 終止。

通過以上示例,我們可以發(fā)現(xiàn),模糊測試之所以強(qiáng)大,就是因?yàn)槠鋾恢眻?zhí)行,不斷生成測試參數(shù),以覆蓋更多的情況和邊界條件。

也正因?yàn)槿绱?,模糊測試通常不建議在 CI 中執(zhí)行。

不過,我們可以使用 -fuzztime 限制模糊測試執(zhí)行的時(shí)間:

go test -fuzz="FuzzHello" -fuzztime 10s
fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed
fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 470505 (156828/sec), new interesting: 0 (total: 3)
fuzz: elapsed: 6s, execs: 948821 (159392/sec), new interesting: 0 (total: 3)
fuzz: elapsed: 9s, execs: 1423720 (158326/sec), new interesting: 0 (total: 3)
fuzz: elapsed: 10s, execs: 1573524 (139348/sec), new interesting: 0 (total: 3)
PASS
ok      github.com/jianghushinian/blog-go-example/test/getting-started/hello    11.820s

這次,我沒有按 ^C 鍵終止測試,而是 10 秒過后,模糊測試自動終止。

現(xiàn)在,我們修改下 Hello 函數(shù),使其返回一個(gè)未知的錯(cuò)誤:

func Hello(name string) (string, error) {
	if name == "" {
		return "", ErrEmptyName
	}
	if len(name) > 10 {
		return "", ErrTooLongName
	}
	if name == "Bob" {
		return "", errors.New("not allowed")
	}
	return "Hello " + name, nil
}

當(dāng) name 值為 Bob 時(shí),Hello 函數(shù)將返回一個(gè)未知錯(cuò)誤,模擬 BUG 場景。

再次使用 go test 命令執(zhí)行模糊測試:

$ go test -fuzz="FuzzHello"              
fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed
fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 8 workers
fuzz: minimizing 30-byte failing input file
fuzz: elapsed: 1s, minimizing
--- FAIL: FuzzHello (1.17s)
    --- FAIL: FuzzHello (0.00s)
        hello_fuzz_test.go:18: unexpected error: not allowed, name: Bob
    Failing input written to testdata/fuzz/FuzzHello/19f92ff5a07664a0
    To re-run:
    go test -run=FuzzHello/19f92ff5a07664a0
FAIL
exit status 1
FAIL    github.com/jianghushinian/blog-go-example/test/getting-started/hello    1.626s

可以發(fā)現(xiàn),這次模糊測試失敗了。

根據(jù)測試結(jié)果,是在執(zhí)行 FuzzHello/19f92ff5a07664a0 時(shí)失敗的,19f92ff5a07664a0 是模糊測試生成的文件,位于 testdata/fuzz/FuzzHello/19f92ff5a07664a0。

使用 tree 命令查看 19f92ff5a07664a0 位置:

 tree hello
hello
├── hello.go
├── hello_fuzz_test.go
└── testdata
    └── fuzz
        └── FuzzHello
            └── 19f92ff5a07664a0

testdata 目錄及目錄下所有內(nèi)容都是模糊測試自動生成的。

19f92ff5a07664a0 文件內(nèi)容如下:

go test fuzz v1
string("Bob")

文件第一行 go test fuzz v1 是模糊測試要求的文件頭,用于標(biāo)識這是一個(gè)種子語料文件,并且使用的編解碼器的版本為 v1。

第二行就是種子語料,是一個(gè) Go 代碼片段,即 string 類型的 Bob 參數(shù)。正是這個(gè)參數(shù),引發(fā)了錯(cuò)誤。

至此,我們使用模糊測試發(fā)現(xiàn)了 Hello 函數(shù)中隱藏的 BUG,這在黑盒測試中尤其有效,我們無需查看 Hello 函數(shù)內(nèi)部代碼,為每個(gè)邊界條件編寫測試用例,模糊測試會自動生成大量的隨機(jī)參數(shù),檢測程序的異常。

測試覆蓋率

go test 命令支持使用 -cover 標(biāo)志查看測試覆蓋率:

$ go test -cover ./... 
?       github.com/jianghushinian/blog-go-example/test/getting-started  [no test files]
ok      github.com/jianghushinian/blog-go-example/test/getting-started/abs      2.334s  coverage: 100.0% of statements
ok      github.com/jianghushinian/blog-go-example/test/getting-started/animal   1.924s  coverage: 100.0% of statements
ok      github.com/jianghushinian/blog-go-example/test/getting-started/hello    2.738s  coverage: 60.0% of statements
ok      github.com/jianghushinian/blog-go-example/test/getting-started/test/abs 3.136s  coverage: [no statements]

注意:根據(jù)我的實(shí)際測試結(jié)果來看,測試覆蓋率默認(rèn)僅包含單元測試、示例測試和模糊測試(模糊測試僅執(zhí)行 f.Add 添加的種子參數(shù)測試),基準(zhǔn)測試并不會被統(tǒng)計(jì)。要想將基準(zhǔn)測試納入覆蓋率統(tǒng)計(jì),需要增加 -bench 參數(shù)。你可以增加 -v 函數(shù)查看更詳細(xì)信息。

此外,go test 命令還支持使用 -coverprofile 參數(shù)生成覆蓋率 profile 文件:

$ go test -coverprofile=coverage.out ./...
?       github.com/jianghushinian/blog-go-example/test/getting-started  [no test files]
ok      github.com/jianghushinian/blog-go-example/test/getting-started/abs      3.101s  coverage: 100.0% of statements
ok      github.com/jianghushinian/blog-go-example/test/getting-started/animal   3.495s  coverage: 100.0% of statements
ok      github.com/jianghushinian/blog-go-example/test/getting-started/hello    3.910s  coverage: 60.0% of statements
ok      github.com/jianghushinian/blog-go-example/test/getting-started/test/abs 4.312s  coverage: [no statements]

命令執(zhí)行后,將在當(dāng)前目錄生成一個(gè) coverage.out 文件,內(nèi)容如下:

cat coverage.out   
mode: set
github.com/jianghushinian/blog-go-example/test/getting-started/abs/abs.go:5.29,7.2 1 1
github.com/jianghushinian/blog-go-example/test/getting-started/animal/animal.go:7.32,8.21 1 1
github.com/jianghushinian/blog-go-example/test/getting-started/animal/animal.go:8.21,10.3 1 1
github.com/jianghushinian/blog-go-example/test/getting-started/animal/animal.go:11.2,11.21 1 1
github.com/jianghushinian/blog-go-example/test/getting-started/animal/animal.go:11.21,13.3 1 1
github.com/jianghushinian/blog-go-example/test/getting-started/animal/animal.go:14.2,14.17 1 1
github.com/jianghushinian/blog-go-example/test/getting-started/hello/hello.go:10.41,11.16 1 1
github.com/jianghushinian/blog-go-example/test/getting-started/hello/hello.go:11.16,13.3 1 0
github.com/jianghushinian/blog-go-example/test/getting-started/hello/hello.go:14.2,14.20 1 1
github.com/jianghushinian/blog-go-example/test/getting-started/hello/hello.go:14.20,16.3 1 0
github.com/jianghushinian/blog-go-example/test/getting-started/hello/hello.go:17.2,17.29 1 1

有了 coverage.out 文件,我們可以直接使用 go tool cover 命令查看測試覆蓋率:

$ go tool cover -func=coverage.out                       
github.com/jianghushinian/blog-go-example/test/getting-started/abs/abs.go:5:            Abs             100.0%
github.com/jianghushinian/blog-go-example/test/getting-started/animal/animal.go:7:      shout           100.0%
github.com/jianghushinian/blog-go-example/test/getting-started/hello/hello.go:10:       Hello           60.0%
total:                                                                                  (statements)    81.8%

以上方式,實(shí)現(xiàn)了在命令行查看程序測試覆蓋率。

我們還可以通過 go tool cover 命令以可視化的方式查看測試覆蓋率:

$ go tool cover -html=coverage.out -o=coverage.html

執(zhí)行命令后,會在當(dāng)前目錄下生成 coverage.html 文件,使用瀏覽器打開內(nèi)容如下:

在頁面頂部左側(cè),可以切換查看不同的測試文件和對應(yīng)測試覆蓋率。

灰色代碼表示未被跟蹤 not tracked。

紅色部分表示未被測試的代碼 not covered。

綠色部分表示已經(jīng)被測試覆蓋的代碼 covered

這樣,我們就可以更加直觀的查看和分析代碼測試覆蓋率了。

總結(jié)

本文向大家介紹了 Go 中編寫各種測試代碼的方式。

Go 支持單元測試、基準(zhǔn)測試、示例測試以及模糊測試四種測試方法。

單元測試是我們最常使用的測試方法,如果被測代碼需要編寫多個(gè)測試用例,可以使用表格測試。

基準(zhǔn)測試能夠測量程序的性能指標(biāo),默認(rèn)情況下 go test 不會執(zhí)行基準(zhǔn)測試,需要指定 -bench regexp 參數(shù)才可以執(zhí)行。

示例測試可以測試程序的標(biāo)準(zhǔn)輸出內(nèi)容,并且能夠配合 pkgsite 工具,在查看本地包文檔時(shí)作為被測函數(shù)文檔的一部分。

模糊測試是 Go 1.18 版本引入的,是一種基于隨機(jī)輸入的自動化測試技術(shù),非常強(qiáng)大,適合用于發(fā)現(xiàn)處理用戶輸入的代碼中存在的問題。

根據(jù)測試代碼與被測代碼是否在同一個(gè)包中,測試又可以分為白盒測試和黑盒測試,我們應(yīng)該盡量編寫白盒測試。

可以使用 go test -cover 查看測試覆蓋率,我們可以將測試覆蓋率基線集成到 CI 中,來保證單元測試覆蓋率。

go test 命令支持的更多參數(shù)可以通過 go help testflag 命令查看。

由于篇幅所限,本文僅算做是 Go 單元測試的基礎(chǔ)入門,更多單元測試在實(shí)戰(zhàn)場景中的應(yīng)用,我會在后續(xù)文章中進(jìn)行講解,敬請期待。

本文完整代碼示例:blog-go-example/test/getting-started at main · jianghushinian/blog-go-example · GitHub

以上就是在Go中編寫測試代碼的方法總結(jié)的詳細(xì)內(nèi)容,更多關(guān)于Go編寫測試代碼的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • go語言操作redis連接池的方法

    go語言操作redis連接池的方法

    這篇文章主要介紹了go語言操作redis連接池的方法,涉及Go語言操作radis的技巧,需要的朋友可以參考下
    2015-03-03
  • 詳解Go hash算法的支持

    詳解Go hash算法的支持

    這篇文章主要介紹了詳解Go hash算法的支持,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2018-09-09
  • Go操作各大消息隊(duì)列教程(RabbitMQ、Kafka)

    Go操作各大消息隊(duì)列教程(RabbitMQ、Kafka)

    消息隊(duì)列是一種異步的服務(wù)間通信方式,適用于無服務(wù)器和微服務(wù)架構(gòu),本文主要介紹了Go操作各大消息隊(duì)列教程(RabbitMQ、Kafka),需要的朋友可以了解一下
    2024-02-02
  • Go讀取yaml文件到struct類的實(shí)現(xiàn)方法

    Go讀取yaml文件到struct類的實(shí)現(xiàn)方法

    本文主要介紹了Go讀取yaml文件到struct類,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-01-01
  • go責(zé)任鏈行為型設(shè)計(jì)模式Chain?Of?Responsibility

    go責(zé)任鏈行為型設(shè)計(jì)模式Chain?Of?Responsibility

    這篇文章主要為大家介紹了go行為型設(shè)計(jì)模式之責(zé)任鏈Chain?Of?Responsibility使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-12-12
  • Golang通脈之map詳情

    Golang通脈之map詳情

    這篇文章主要介紹了Golang通脈之map,Go語言中提供的映射關(guān)系容器為map,其內(nèi)部使用散列表(hash)實(shí)現(xiàn),map 是一種無序的鍵值對的集合。map 最重要的一點(diǎn)是通過 key 來快速檢索數(shù)據(jù),key 類似于索引,指向數(shù)據(jù)的值 map 是一種集合,所以可以像迭代數(shù)組和切片那樣迭代它
    2021-10-10
  • 基于Go語言開發(fā)一個(gè)編解碼工具

    基于Go語言開發(fā)一個(gè)編解碼工具

    這篇文章主要為大家詳細(xì)介紹了如何基于Go語言開發(fā)一個(gè)編解碼工具,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,感興趣的小伙伴可以跟隨小編一起了解一下
    2025-03-03
  • go Antlr重構(gòu)腳本解釋器實(shí)現(xiàn)示例

    go Antlr重構(gòu)腳本解釋器實(shí)現(xiàn)示例

    這篇文章主要為大家介紹了go Antlr重構(gòu)腳本解釋器實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-08-08
  • golang 并發(fā)編程之生產(chǎn)者消費(fèi)者詳解

    golang 并發(fā)編程之生產(chǎn)者消費(fèi)者詳解

    這篇文章主要介紹了golang 并發(fā)編程之生產(chǎn)者消費(fèi)者詳解,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-05-05
  • Go調(diào)度器學(xué)習(xí)之goroutine調(diào)度詳解

    Go調(diào)度器學(xué)習(xí)之goroutine調(diào)度詳解

    這篇文章主要為大家詳細(xì)介紹了Go調(diào)度器中g(shù)oroutine調(diào)度的相關(guān)知識,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2023-03-03

最新評論