Kiểm thử
Đối với nhà phát triển kiểm thử tốt có thể phát hiện sớm lỗi trong chương trình tránh gánh nặng tinh thần do bảo trì không kịp thời gây ra Bug vì vậy viết kiểm thử rất cần thiết. Go cung cấp công cụ dòng lệnh rất tiện lợi và thiết thực trong khía cạnh kiểm thử là go test trong thư viện chuẩn và nhiều framework mã nguồn mở đều có thể thấy bóng dáng của kiểm thử công cụ này sử dụng rất tiện lợi hiện hỗ trợ các loại kiểm thử sau
- Kiểm thử ví dụ
- Kiểm thử đơn vị
- Kiểm thử benchmark
- Kiểm thử fuzzing
Trong Go phần lớn API do thư viện chuẩn testing cung cấp.
TIP
Thực hiện lệnh go help testfunc trong dòng lệnh có thể xem giải thích của Go chính thức về bốn loại kiểm thử ở trên.
Quy tắc viết
Trước khi bắt đầu viết kiểm thử trước tiên cần lưu ý vài quy tắc như vậy sẽ tiện lợi hơn trong quá trình học tập sau này.
- Gói kiểm thử file kiểm thử tốt nhất nên để riêng trong một gói gói này thường được đặt tên là
test. - File kiểm thử file kiểm thử thường kết thúc bằng
_test.goví dụ muốn kiểm thử một chức năng nào đó thì đặt tên nó làfunction_test.gonếu muốn chia nhỏ hơn theo loại kiểm thử cũng có thể lấy loại kiểm thử làm tiền tố file ví dụbenchmark_marshaling_test.gohoặcexample_marshaling_test.go. - Hàm kiểm thử mỗi file kiểm thử đều có若干 hàm kiểm thử dùng cho các kiểm thử khác nhau. Đối với các loại kiểm thử khác nhau phong cách đặt tên của hàm kiểm thử cũng khác nhau. Ví dụ kiểm thử ví dụ là
ExampleXXXXkiểm thử đơn vị làTestXXXXkiểm thử benchmark làBenchmarkXXXXkiểm thử fuzzing làFuzzXXXXnhư vậy dù không cần chú thích cũng có thể biết đây là loại kiểm thử gì.
TIP
Khi tên gói là testdata gói này thường là để lưu trữ dữ liệu hỗ trợ dùng cho kiểm thử khi thực hiện kiểm thử Go sẽ bỏ qua gói tên là testdata.
Tuân thủ các quy tắc trên nuôi dưỡng phong cách kiểm thử tốt có thể tiết kiệm không ít phiền phức cho việc bảo trì sau này.
Thực hiện kiểm thử
Thực hiện kiểm thử chủ yếu sử dụng lệnh go test dưới đây lấy mã thực tế để minh họa hiện có file chờ kiểm thử /say/hello.go mã như sau
package say
import "fmt"
func Hello() {
fmt.Println("hello")
}
func GoodBye() {
fmt.Println("bye")
}Và file kiểm thử /test/example_test.go mã như sau
package test
import (
"golearn/say"
)
func ExampleHello() {
say.Hello()
// Output:
// hello
}
func ExampleGoodBye() {
say.GoodBye()
// Output:
// bye
}
func ExampleSay() {
say.Hello()
say.GoodBye()
// Output:
// hello
// bye
}Có nhiều cách để thực hiện các kiểm thử này ví dụ muốn thực hiện tất cả các ca kiểm thử trong gói test có thể trực tiếp thực hiện lệnh sau trong thư mục test
$ go test ./
PASS
ok golearn/test 0.422s./ biểu thị thư mục hiện tại Go sẽ biên dịch lại tất cả các file kiểm thử trong thư mục test sau đó thực hiện tất cả các ca kiểm thử từ kết quả có thể thấy tất cả các ca kiểm thử đều đã vượt qua. Tham số sau đó cũng có thể theo nhiều thư mục ví dụ lệnh dưới đây rõ ràng thư mục chính của dự án không có file kiểm thử可供 thực hiện.
$ go test ./ ../
ok golearn/test
? golearn [no test files]TIP
Khi tham số thực hiện có nhiều gói Go sẽ không thực hiện lại các ca kiểm thử đã thành công vượt qua khi thực hiện sẽ thêm (cached) ở cuối dòng để biểu thị kết quả xuất là cache của lần trước. Khi cờ kiểm thử nằm trong tập hợp sau Go sẽ cache kết quả kiểm thử nếu không thì sẽ không.
-benchtime, -cpu,-list, -parallel, -run, -short, -timeout, -failfast, -vNếu muốn vô hiệu hóa cache có thể thêm tham số -count=1.
Đương nhiên cũng có thể chỉ định riêng một file kiểm thử nào đó để thực hiện.
$ go test example_test.go
ok command-line-arguments 0.457sHoặc có thể chỉ định riêng một ca kiểm thử nào đó của một file kiểm thử nào đó ví dụ
$ go test -run ExampleSay
PASS
ok golearn/test 0.038sBa trường hợp trên tuy đều hoàn thành kiểm thử nhưng kết quả xuất quá ngắn gọn lúc này có thể thêm tham số -v để kết quả xuất chi tiết hơn một chút ví dụ
$ go test ./ -v
=== RUN ExampleHello
--- PASS: ExampleHello (0.00s)
=== RUN ExampleGoodBye
--- PASS: ExampleGoodBye (0.00s)
=== RUN ExampleSay
--- PASS: ExampleSay (0.00s)
PASS
ok golearn/test 0.040sNhư vậy có thể thấy rõ thứ tự thực hiện thời gian tiêu thụ tình hình thực hiện của mỗi ca kiểm thử cũng như tổng thời gian tiêu thụ.
TIP
Lệnh go test mặc định thực hiện tất cả kiểm thử đơn vị kiểm thử ví dụ kiểm thử fuzzing nếu thêm tham số -bench thì sẽ thực hiện tất cả các loại kiểm thử ví dụ lệnh dưới đây
$ go test -bench .Vì vậy cần sử dụng tham số -run để chỉ định ví dụ chỉ thực hiện tất cả kiểm thử benchmark lệnh như sau
$ go test -bench . -run ^$Tham số thông dụng
Kiểm thử Go có rất nhiều cờ tham số dưới đây chỉ giới thiệu các tham số thông dụng muốn biết thêm chi tiết nên sử dụng lệnh go help testflag để tự tra cứu.
| Tham số | Giải thích |
|---|---|
-o file | Chỉ định tên file nhị phân sau khi biên dịch |
-c | Chỉ biên dịch file kiểm thử nhưng không chạy |
-json | Xuất log kiểm thử dưới dạng json |
-exec xprog | Sử dụng xprog để chạy kiểm thử tương đương với go run |
-bench regexp | Chọn kiểm thử benchmark khớp với regexp |
-fuzz regexp | Chọn kiểm thử fuzzing khớp với regexp |
-fuzztime t | Thời gian tự động kết thúc của kiểm thử fuzzing t là khoảng thời gian khi đơn vị là x biểu thị số lần ví dụ 200x |
-fuzzminimizetime t | Thời gian chạy tối thiểu của kiểm thử mode quy tắc như trên |
-count n | Chạy kiểm thử n lần mặc định 1 lần |
-cover | Bật phân tích độ phủ kiểm thử |
-covermode set,count,atomic | Đặt mode phân tích độ phủ |
-cpu | Thực hiện GOMAXPROCS cho thực thi kiểm thử |
-failfast | Sau khi kiểm thử thất bại lần đầu sẽ không bắt đầu kiểm thử mới |
-list regexp | Liệt kê ca kiểm thử khớp với regexp |
-parallel n | Cho phép ca kiểm thử đã gọi t.Parallel chạy song song n là số lượng tối đa song song |
-run regexp | Chỉ chạy ca kiểm thử khớp với regexp |
-skip regexp | Bỏ qua ca kiểm thử khớp với regexp |
-timeout d | Nếu thời gian thực thi đơn kiểm thử vượt quá khoảng thời gian d sẽ panic. d là khoảng thời gian ví dụ 1s,1ms,1ns v.v. |
-shuffle off,on,N | Xáo trộn thứ tự thực thi kiểm thử N là hạt giống ngẫu nhiên mặc định hạt giống là thời gian hệ thống |
-v | Xuất log kiểm thử chi tiết hơn |
-benchmem | Thống kê phân bổ bộ nhớ của kiểm thử benchmark |
-blockprofile block.out | Thống kê tình hình block goroutine trong kiểm thử và ghi vào file |
-blockprofilerate n | Kiểm soát tần suất thống kê block goroutine qua lệnh go doc runtime.SetBlockProfileRate xem thêm chi tiết |
-coverprofile cover.out | Thống kê tình hình kiểm thử độ phủ và ghi vào file |
-cpuprofile cpu.out | Thống kê tình hình cpu và ghi vào file |
-memprofile mem.out | Thống kê tình hình phân bổ bộ nhớ và ghi vào file |
-memprofilerate n | Kiểm soát tần suất thống kê phân bổ bộ nhớ qua lệnh go doc runtime.MemProfileRate xem thêm chi tiết |
-mutexprofile mutex.out | Thống kê tình hình cạnh tranh lock và ghi vào file |
-mutexprofilefraction n | Đặt thống kê tình hình n goroutine cạnh tranh một mutex |
-trace trace.out | Ghi tình hình theo dõi thực thi vào file |
-outputdir directory | Chỉ định thư mục xuất của các file thống kê ở trên mặc định là thư mục chạy của go test |
Kiểm thử ví dụ
Kiểm thử ví dụ không giống ba loại kiểm thử khác là để phát hiện vấn đề của chương trình nó nhiều hơn là để hiển thị cách sử dụng của một chức năng nào đó đóng vai trò tài liệu. Kiểm thử ví dụ không phải là một khái niệm chính thức định nghĩa cũng không phải là một quy định cứng nhắc giống như một thỏa thuận kỹ thuật trong engineering việc có tuân thủ hay không chỉ phụ thuộc vào nhà phát triển. Kiểm thử ví dụ xuất hiện rất nhiều trong thư viện chuẩn thường là ví dụ mã thư viện chuẩn do chính thức biên viết ví dụ hàm kiểm thử ExampleWithDeadline trong context/example_test.go của thư viện chuẩn hàm kiểm thử này thể hiện cách sử dụng cơ bản của DeadlineContext
// This example passes a context with an arbitrary deadline to tell a blocking
// function that it should abandon its work as soon as it gets to it.
func ExampleWithDeadline() {
d := time.Now().Add(shortDuration)
ctx, cancel := context.WithDeadline(context.Background(), d)
// Even though ctx will be expired, it is good practice to call its
// cancellation function in any case. Failure to do so may keep the
// context and its parent alive longer than necessary.
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
// Output:
// context deadline exceeded
}Bề ngoài mà nói hàm kiểm thử này giống như một hàm bình thường nhưng kiểm thử ví dụ chủ yếu được thể hiện qua chú thích Output hàm chờ kiểm thử chỉ có một dòng xuất sử dụng chú thích Output để kiểm tra xuất. Trước tiên tạo một file tên là hello.go ghi mã như sau
package say
import "fmt"
func Hello() {
fmt.Println("hello")
}
func GoodBye() {
fmt.Println("bye")
}Hàm SayHello là hàm chờ kiểm tra sau đó tạo file kiểm thử example_test.go ghi mã như sau
package test
import (
"golearn/say"
)
func ExampleHello() {
say.Hello()
// Output:
// hello
}
func ExampleGoodBye() {
say.GoodBye()
// Output:
// bye
}
func ExampleSay() {
say.Hello()
say.GoodBye()
// Output:
// hello
// bye
}Chú thích Output trong hàm biểu thị kiểm tra xuất của hàm này có phải là hello không tiếp theo thực hiện lệnh kiểm thử xem kết quả.
$ go test -v
=== RUN ExampleHello
--- PASS: ExampleHello (0.00s)
=== RUN ExampleGoodBye
--- PASS: ExampleGoodBye (0.00s)
=== RUN ExampleSay
--- PASS: ExampleSay (0.00s)
PASS
ok golearn/test 0.448sTừ kết quả có thể thấy tất cả kiểm thử đều đã vượt qua về Output có vài cách viết sau đây cách viết thứ nhất là chỉ có một dòng xuất ý là kiểm tra xuất của hàm này có phải hello không
// Output:
// helloCách viết thứ hai là nhiều dòng xuất tức kiểm tra khớp theo thứ tự
// Output:
// hello
// byeCách viết thứ ba là xuất không theo thứ tự tức khớp xuất nhiều dòng không theo thứ tự
// Unordered output:
// bye
// helloCần lưu ý đối với hàm kiểm thử mà nói chỉ khi vài dòng cuối cùng là chú thích Output mới được xem là kiểm thử ví dụ nếu không thì chỉ là một hàm bình thường sẽ không được Go thực hiện.
Kiểm thử đơn vị
Kiểm thử đơn vị là kiểm tra đơn vị có thể kiểm thử nhỏ nhất trong phần mềm kích thước của đơn vị phụ thuộc vào nhà phát triển có thể là một struct hoặc là một gói cũng có thể là một hàm hoặc là một loại. Dưới đây vẫn thông qua ví dụ để minh họa trước tiên tạo file /tool/math.go ghi mã như sau
package tool
type Number interface {
~int8 | ~int16 | ~int32 | ~int64 | ~int
}
func SumInt[T Number](a, b T) T {
return a + b
}
func Equal[T Number](a, b T) bool {
return a == b
}Sau đó tạo file kiểm thử /tool_test/unit_test.go đối với kiểm thử đơn vị mà nói đặt tên có thể là unit_test hoặc lấy gói muốn kiểm thử hoặc chức năng làm tiền tố file.
package test_test
import (
"golearn/tool"
"testing"
)
func TestSum(t *testing.T) {
a, b := 10, 101
expected := 111
actual := tool.SumInt(a, b)
if actual != expected {
t.Errorf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
}
}
func TestEqual(t *testing.T) {
a, b := 10, 101
expected := false
actual := tool.Equal(a, b)
if actual != expected {
t.Errorf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
}
}Đối với kiểm thử đơn vị mà nói phong cách đặt tên của mỗi ca kiểm thử là TestXXXX và tham số đầu vào của hàm phải là t *testing.T testing.T là struct do gói testing cung cấp dùng để thuận tiện kiểm thử cung cấp nhiều phương pháp khả dụng ví dụ trong ví dụ t.Errorf tương đương với t.Logf dùng để xuất thông tin log kiểm thử thất bại theo định dạng các phương pháp thông dụng khác còn có t.Fail dùng để đánh dấu ca kiểm thử hiện tại là thất bại chức năng tương tự còn có t.FailNow cũng sẽ đánh dấu là kiểm thử thất bại nhưng cái trước sau khi thất bại vẫn tiếp tục thực hiện cái sau thì sẽ trực tiếp dừng thực hiện như ví dụ dưới đây sửa kết quả mong đợi thành kết quả sai
package tool_test
import (
"golearn/tool"
"testing"
)
func TestSum(t *testing.T) {
a, b := 10, 101
expected := 110
actual := tool.SumInt(a, b)
if actual != expected {
// Errorf sử dụng t.Fail() bên trong
t.Errorf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
}
t.Log("test finished")
}
func TestEqual(t *testing.T) {
a, b := 10, 101
expected := true
actual := tool.Equal(a, b)
if actual != expected {
// Fatalf sử dụng t.FailNow() bên trong
t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
}
t.Log("test finished")
}Thực hiện kiểm thử trên xuất như sau
$ go test tool_test.go -v
=== RUN TestSum
tool_test.go:14: Sum(10,101) expected 110,actual is 111
tool_test.go:16: test finished
--- FAIL: TestSum (0.00s)
=== RUN TestEqual
tool_test.go:25: Sum(10,101) expected true,actual is false
--- FAIL: TestEqual (0.00s)
FAIL command-line-arguments 0.037sTừ log kiểm thử có thể thấy TestSum tuy thất bại vẫn xuất test finished còn TestEqual thì không tương tự còn có t.SkipNow sẽ đánh dấu ca kiểm thử hiện tại là SKIP sau đó dừng thực hiện trong vòng kiểm thử tiếp theo sẽ tiếp tục thực hiện.
package tool_test
import (
"golearn/tool"
"testing"
)
func TestSum(t *testing.T) {
a, b := 10, 101
expected := 110
actual := tool.SumInt(a, b)
if actual != expected {
t.Skipf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
}
t.Log("test finished")
}
func TestEqual(t *testing.T) {
a, b := 10, 101
expected := true
actual := tool.Equal(a, b)
if actual != expected {
t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
}
t.Log("test finished")
}Khi thực hiện kiểm thử sửa số lần kiểm thử thành 2
$ go test tool_test.go -v -count=2
=== RUN TestSum
tool_test.go:14: Sum(10,101) expected 110,actual is 111
--- SKIP: TestSum (0.00s)
=== RUN TestEqual
tool_test.go:25: Sum(10,101) expected true,actual is false
--- FAIL: TestEqual (0.00s)
=== RUN TestSum
tool_test.go:14: Sum(10,101) expected 110,actual is 111
--- SKIP: TestSum (0.00s)
=== RUN TestEqual
tool_test.go:25: Sum(10,101) expected true,actual is false
--- FAIL: TestEqual (0.00s)
FAIL command-line-arguments 0.468sVí dụ trên xuất test finished ở dòng cuối cùng dùng để biểu thị kiểm thử xong thực ra có thể sử dụng t.Cleanup để đăng ký một hàm收尾 chuyên làm việc này hàm này sẽ thực hiện khi kết thúc ca kiểm thử như sau.
package tool_test
import (
"golearn/tool"
"testing"
)
func finished(t *testing.T) {
t.Log("test finished")
}
func TestSum(t *testing.T) {
t.Cleanup(func() {
finished(t)
})
a, b := 10, 101
expected := 111
actual := tool.SumInt(a, b)
if actual != expected {
t.Skipf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
}
}
func TestEqual(t *testing.T) {
t.Cleanup(func() {
finished(t)
})
a, b := 10, 101
expected := false
actual := tool.Equal(a, b)
if actual != expected {
t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
}
}Thực hiện kiểm thử sau xuất như sau
$ go test tool_test.go -v
=== RUN TestSum
tool_test.go:9: test finished
--- PASS: TestSum (0.00s)
=== RUN TestEqual
tool_test.go:9: test finished
--- PASS: TestEqual (0.00s)
PASS
ok command-line-arguments 0.462sHelper
Thông qua t.Helper() có thể đánh dấu hàm hiện tại là hàm trợ giúp hàm trợ giúp sẽ không单独 làm một ca kiểm thử dùng để thực hiện khi ghi log số dòng xuất ra cũng là số dòng của người gọi hàm trợ giúp như vậy có thể khiến phân tích log định vị chính xác hơn tránh thông tin dài dòng tạp nham khác. Ví dụ t.Cleanup ở trên có thể sửa thành hàm trợ giúp như sau.
package tool_test
import (
"golearn/tool"
"testing"
)
func CleanupHelper(t *testing.T) {
t.Helper()
t.Log("test finished")
}
func TestSum(t *testing.T) {
t.Cleanup(func() {
CleanupHelper(t)
})
a, b := 10, 101
expected := 111
actual := tool.SumInt(a, b)
if actual != expected {
t.Skipf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
}
}
func TestEqual(t *testing.T) {
t.Cleanup(func() {
CleanupHelper(t)
})
a, b := 10, 101
expected := false
t.Helper()
actual := tool.Equal(a, b)
if actual != expected {
t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
}
}Thực hiện kiểm thử sau thông tin xuất như sau khác biệt với trước đó là số dòng test finished trở thành số dòng của người gọi.
$ go test tool_test.go -v
=== RUN TestSum
tool_test.go:15: test finished
--- PASS: TestSum (0.00s)
=== RUN TestEqual
tool_test.go:30: test finished
--- PASS: TestEqual (0.00s)
PASS
ok command-line-arguments 0.464sTIP
Các thao tác trên chỉ có thể thực hiện trong kiểm thử chính tức ca kiểm thử trực tiếp thực hiện nếu sử dụng trong kiểm thử con sẽ panic.
Kiểm thử con
Trong một số trường hợp sẽ cần kiểm tra thêm ca kiểm thử khác trong một ca kiểm thử ca kiểm thử lồng nhau này nói chung được gọi là kiểm thử con thông qua phương pháp t.Run() chữ ký hàm như sau
// Phương pháp Run sẽ khởi động một goroutine mới dùng để chạy kiểm thử con chặn chờ hàm f thực hiện xong mới trả về
// Giá trị trả về là có vượt qua kiểm thử không
func (t *T) Run(name string, f func(t *T)) boolDưới đây là một ví dụ
func TestTool(t *testing.T) {
t.Run("tool.Sum(10,101)", TestSum)
t.Run("tool.Equal(10,101)", TestEqual)
}Thực hiện sau kết quả như sau
$ go test -run TestTool -v
=== RUN TestTool
=== RUN TestTool/tool.Sum(10,101)
tool_test.go:15: test finished
=== RUN TestTool/tool.Equal(10,101)
tool_test.go:30: test finished
--- PASS: TestTool (0.00s)
--- PASS: TestTool/tool.Sum(10,101) (0.00s)
--- PASS: TestTool/tool.Equal(10,101) (0.00s)
PASS
ok golearn/tool_test 0.449sThông qua kết quả xuất có thể thấy rõ cấu trúc层级 cha con ca kiểm thử con thứ nhất trong ví dụ trên chưa thực hiện xong ca kiểm thử con thứ hai sẽ không thực hiện có thể sử dụng t.Parallel() để đánh dấu ca kiểm thử là có thể chạy song song như vậy thứ tự xuất sẽ không thể xác định.
package tool_test
import (
"golearn/tool"
"testing"
)
func CleanupHelper(t *testing.T) {
t.Helper()
t.Log("test finished")
}
func TestSum(t *testing.T) {
t.Parallel()
t.Cleanup(func() {
CleanupHelper(t)
})
a, b := 10, 101
expected := 111
actual := tool.SumInt(a, b)
if actual != expected {
t.Skipf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
}
}
func TestEqual(t *testing.T) {
t.Parallel()
t.Cleanup(func() {
CleanupHelper(t)
})
a, b := 10, 101
expected := false
actual := tool.Equal(a, b)
if actual != expected {
t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
}
}
func TestToolParallel(t *testing.T) {
t.Log("setup")
t.Run("tool.Sum(10,101)", TestSum)
t.Run("tool.Equal(10,101)", TestEqual)
t.Log("teardown")
}Thực hiện kiểm thử sau xuất như sau
$ go test -run TestTool -v
=== RUN TestToolParallel
tool_test.go:46: setup
=== RUN TestToolParallel/tool.Sum(10,101)
=== PAUSE TestToolParallel/tool.Sum(10,101)
=== RUN TestToolParallel/tool.Equal(10,101)
=== PAUSE TestToolParallel/tool.Equal(10,101)
=== NAME TestToolParallel
tool_test.go:49: teardown
=== CONT TestToolParallel/tool.Sum(10,101)
=== CONT TestToolParallel/tool.Equal(10,101)
=== NAME TestToolParallel/tool.Sum(10,101)
tool_test.go:16: test finished
=== NAME TestToolParallel/tool.Equal(10,101)
tool_test.go:32: test finished
--- PASS: TestToolParallel (0.00s)
--- PASS: TestToolParallel/tool.Sum(10,101) (0.00s)
--- PASS: TestToolParallel/tool.Equal(10,101) (0.00s)
PASS
ok golearn/tool_test 0.444sTừ kết quả kiểm thử có thể thấy rõ có một quá trình chặn chờ khi thực hiện ca kiểm thử song song như ví dụ trên chắc chắn không thể tiến hành bình thường vì mã tiếp theo không thể đảm bảo chạy đồng bộ lúc này có thể chọn lồng thêm một lớp t.Run() nữa như sau
func TestToolParallel(t *testing.T) {
t.Log("setup")
t.Run("process", func(t *testing.T) {
t.Run("tool.Sum(10,101)", TestSum)
t.Run("tool.Equal(10,101)", TestEqual)
})
t.Log("teardown")
}Thực hiện lại có thể thấy kết quả thực thi bình thường.
$ go test -run TestTool -v
=== RUN TestToolParallel
tool_test.go:46: setup
=== RUN TestToolParallel/process
=== RUN TestToolParallel/process/tool.Sum(10,101)
=== PAUSE TestToolParallel/process/tool.Sum(10,101)
=== RUN TestToolParallel/process/tool.Equal(10,101)
=== PAUSE TestToolParallel/process/tool.Equal(10,101)
=== CONT TestToolParallel/process/tool.Sum(10,101)
=== CONT TestToolParallel/process/tool.Equal(10,101)
=== NAME TestToolParallel/process/tool.Sum(10,101)
tool_test.go:16: test finished
=== NAME TestToolParallel/process/tool.Equal(10,101)
tool_test.go:32: test finished
=== NAME TestToolParallel
tool_test.go:51: teardown
--- PASS: TestToolParallel (0.00s)
--- PASS: TestToolParallel/process (0.00s)
--- PASS: TestToolParallel/process/tool.Sum(10,101) (0.00s)
--- PASS: TestToolParallel/process/tool.Equal(10,101) (0.00s)
PASS
ok golearn/tool_test 0.450sPhong cách bảng
Trong kiểm thử đơn vị ở trên dữ liệu đầu vào kiểm thử đều là biến được khai báo thủ công từng cái khi dữ liệu nhỏ thì không sao nhưng nếu muốn kiểm tra nhiều bộ dữ liệu thì không thể lại khai báo biến để tạo dữ liệu kiểm thử nên nói chung đều cố gắng sử dụng dạng slice struct struct là struct ẩn danh được khai báo tạm thời vì phong cách mã hóa này nhìn giống như bảng nên gọi là table-driven. Dưới đây lấy ví dụ đây là ví dụ khai báo nhiều biến thủ công để tạo dữ liệu kiểm thử nếu có nhiều bộ dữ liệu nhìn sẽ không trực quan nên sửa thành phong cách bảng
func TestEqual(t *testing.T) {
t.Cleanup(func() {
CleanupHelper(t)
})
a, b := 10, 101
expected := false
actual := tool.Equal(a, b)
if actual != expected {
t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
}
}Mã sau khi sửa như sau
func TestEqual(t *testing.T) {
t.Cleanup(func() {
CleanupHelper(t)
})
// table driven style
testData := []struct {
a, b int
exp bool
}{
{10, 101, false},
{5, 5, true},
{30, 32, false},
{100, 101, false},
{2, 3, false},
{4, 4, true},
}
for _, data := range testData {
if actual := tool.Equal(data.a, data.b); actual != data.exp {
t.Fatalf("Sum(%d,%d) expected %t,actual is %t", data.a, data.b, data.exp, actual)
}
}
}Dữ liệu kiểm thử như vậy nhìn sẽ trực quan hơn nhiều.
Kiểm thử benchmark
Kiểm thử benchmark còn gọi là kiểm thử hiệu suất thường dùng để kiểm tra mức sử dụng bộ nhớ tình hình sử dụng CPU thời gian thực thi v.v. của chương trình. Đối với kiểm thử benchmark mà nói file kiểm thử thường kết thúc bằng bench_test.go còn tên hàm của ca kiểm thử phải là định dạng BenchmarkXXXX.
Dưới đây lấy ví dụ so sánh hiệu suất của nối chuỗi làm ví dụ kiểm thử benchmark. Trước tiên tạo file /tool/strConcat.go như mọi người đã biết trực tiếp sử dụng chuỗi thực hiện nối + hiệu suất rất thấp còn sử dụng strings.Builder thì tốt hơn nhiều trong file /tool/strings.go lần lượt tạo hai hàm thực hiện hai cách nối chuỗi.
package tool
import "strings"
func ConcatStringDirect(longString string) {
res := ""
for i := 0; i < 100_000.; i++ {
res += longString
}
}
func ConcatStringWithBuilder(longString string) {
var res strings.Builder
for i := 0; i < 100_000.; i++ {
res.WriteString(longString)
}
}Sau đó tạo file kiểm thử /tool_test/bench_tool_test.go mã như sau
package tool_test
import (
"golearn/tool"
"testing"
)
var longString = "longStringlongStringlongStringlongStringlongStringlongStringlongStringlongString"
func BenchmarkConcatDirect(b *testing.B) {
for i := 0; i < b.N; i++ {
tool.ConcatStringDirect(longString)
}
}
func BenchmarkConcatBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
tool.ConcatStringWithBuilder(longString)
}
}Thực hiện lệnh kiểm thử lệnh bật log chi tiết và phân tích bộ nhớ chỉ định số lõi CPU sử dụng và mỗi ca kiểm thử thực hiện hai vòng kết quả xuất như sau
$ go test -v -benchmem -bench . -run bench_tool_test.go -cpu=2,4,8 -count=2
goos: windows
goarch: amd64
pkg: golearn/tool_test
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
BenchmarkConcatDirect
BenchmarkConcatDirect-2 4 277771375 ns/op 4040056736 B/op 10000 allocs/op
BenchmarkConcatDirect-2 4 278500125 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-4 1 1153796000 ns/op 4040068784 B/op 10126 allocs/op
BenchmarkConcatDirect-4 1 1211017600 ns/op 4040073104 B/op 10171 allocs/op
BenchmarkConcatDirect-8 2 665460800 ns/op 4040077760 B/op 10219 allocs/op
BenchmarkConcatDirect-8 2 679774450 ns/op 4040080064 B/op 10243 allocs/op
BenchmarkConcatBuilder
BenchmarkConcatBuilder-2 3428 344530 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 3579 351858 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-4 2448 736177 ns/op 4128185 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1688 662993 ns/op 4128185 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1958 550333 ns/op 4128199 B/op 29 allocs/op
BenchmarkConcatBuilder-8 2174 552113 ns/op 4128196 B/op 29 allocs/op
PASS
ok golearn/tool_test 21.381sDưới đây giải thích kết quả xuất của kiểm thử benchmark goos đại diện là hệ điều hành đang chạy goarh đại diện là kiến trúc CPU pkg là gói nơi kiểm thử tọa lạc cpu là một số thông tin về CPU. Kết quả của mỗi ca kiểm thử dưới đây được phân cách bởi tên của mỗi kiểm thử benchmark cột đầu tiên BenchmarkConcatDirect-2 số 2 đại diện là số lõi CPU sử dụng số 4 ở cột thứ hai đại diện là kích thước b.N trong mã tức số vòng lặp trong kiểm thử benchmark cột thứ ba 277771375 ns/op đại diện là thời gian tiêu thụ của mỗi vòng lặp ns là nano giây cột thứ tư 4040056736 B/op biểu thị kích thước byte bộ nhớ được phân bổ của mỗi vòng lặp cột thứ năm 10000 allocs/op biểu thị số lần phân bổ bộ nhớ của mỗi vòng lặp.
Rõ ràng theo kết quả kiểm thử mà nói hiệu suất của việc sử dụng strings.Builder cao hơn nhiều so với sử dụng + nối chuỗi so sánh hiệu suất thông qua dữ liệu trực quan chính là mục đích của kiểm thử benchmark.
benchstat
benchstat là một công cụ phân tích kiểm thử hiệu suất mã nguồn mở số lượng mẫu của kiểm thử hiệu suất ở trên chỉ có hai nhóm một khi số lượng mẫu nhiều lên phân tích thủ công sẽ rất tốn thời gian và công sức công cụ này sinh ra là để giải quyết vấn đề phân tích hiệu suất.
Trước tiên cần tải xuống công cụ này
$ go install golang.org/x/perf/benchstatThực hiện kiểm thử benchmark hai lần lần này sửa số lượng mẫu thành 5 và lần lượt xuất vào file old.txt và new.txt để so sánh kết quả thực hiện lần đầu
$ go test -v -benchmem -bench . -run bench_tool_test.go -cpu=2,4,8 -count=5 | tee -a old.txt
goos: windows
goarch: amd64
pkg: golearn/tool_test
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
BenchmarkConcatDirect
BenchmarkConcatDirect-2 4 290535650 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-2 4 298974625 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-2 4 299637800 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-2 4 276487000 ns/op 4040056784 B/op 10001 allocs/op
BenchmarkConcatDirect-2 4 356465275 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-4 2 894723200 ns/op 4040077424 B/op 10216 allocs/op
BenchmarkConcatDirect-4 2 785830400 ns/op 4040078288 B/op 10225 allocs/op
BenchmarkConcatDirect-4 2 743634000 ns/op 4040077568 B/op 10217 allocs/op
BenchmarkConcatDirect-4 2 953802700 ns/op 4040075408 B/op 10195 allocs/op
BenchmarkConcatDirect-4 2 953028750 ns/op 4040077520 B/op 10217 allocs/op
BenchmarkConcatDirect-8 2 684023150 ns/op 4040086784 B/op 10313 allocs/op
BenchmarkConcatDirect-8 2 634380250 ns/op 4040090528 B/op 10352 allocs/op
BenchmarkConcatDirect-8 2 685030600 ns/op 4040090768 B/op 10355 allocs/op
BenchmarkConcatDirect-8 2 817909650 ns/op 4040089808 B/op 10345 allocs/op
BenchmarkConcatDirect-8 2 600078100 ns/op 4040095664 B/op 10406 allocs/op
BenchmarkConcatBuilder
BenchmarkConcatBuilder-2 2925 419651 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 2961 423899 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 2714 422275 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 2848 452255 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 2612 454452 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-4 974 1158000 ns/op 4128189 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1098 1068682 ns/op 4128192 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1042 1056570 ns/op 4128194 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1280 978213 ns/op 4128191 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1538 1162108 ns/op 4128190 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1744 700824 ns/op 4128203 B/op 29 allocs/op
BenchmarkConcatBuilder-8 2235 759537 ns/op 4128201 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1556 736455 ns/op 4128204 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1592 825794 ns/op 4128201 B/op 29 allocs/op
BenchmarkConcatBuilder-8 2263 717285 ns/op 4128203 B/op 29 allocs/op
PASS
ok golearn/tool_test 56.742sKết quả thực hiện lần hai
$ go test -v -benchmem -bench . -run bench_tool_test.go -cpu=2,4,8 -count=5 | tee -a new.txt
goos: windows
goarch: amd64
pkg: golearn/tool_test
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
BenchmarkConcatDirect
BenchmarkConcatDirect-2 4 285074900 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-2 4 291517150 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-2 4 281901975 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-2 4 292320625 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-2 4 286723000 ns/op 4040056952 B/op 10002 allocs/op
BenchmarkConcatDirect-4 1 1188983000 ns/op 4040071856 B/op 10158 allocs/op
BenchmarkConcatDirect-4 1 1080713900 ns/op 4040070800 B/op 10147 allocs/op
BenchmarkConcatDirect-4 1 1203622300 ns/op 4040067344 B/op 10111 allocs/op
BenchmarkConcatDirect-4 1 1045291300 ns/op 4040070224 B/op 10141 allocs/op
BenchmarkConcatDirect-4 1 1123163300 ns/op 4040070032 B/op 10139 allocs/op
BenchmarkConcatDirect-8 2 790421300 ns/op 4040076656 B/op 10208 allocs/op
BenchmarkConcatDirect-8 2 659047300 ns/op 4040079488 B/op 10237 allocs/op
BenchmarkConcatDirect-8 2 712991800 ns/op 4040077184 B/op 10213 allocs/op
BenchmarkConcatDirect-8 2 706605350 ns/op 4040078000 B/op 10222 allocs/op
BenchmarkConcatDirect-8 2 656195700 ns/op 4040085248 B/op 10297 allocs/op
BenchmarkConcatBuilder
BenchmarkConcatBuilder-2 2726 386412 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 3439 335358 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 3376 338957 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 3870 326301 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 4285 339596 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1663 671535 ns/op 4128187 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1507 744885 ns/op 4128191 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1353 1097800 ns/op 4128187 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1388 1006019 ns/op 4128189 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1635 993764 ns/op 4128189 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1332 783599 ns/op 4128198 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1818 729821 ns/op 4128202 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1398 780614 ns/op 4128202 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1526 750513 ns/op 4128204 B/op 29 allocs/op
BenchmarkConcatBuilder-8 2164 704798 ns/op 4128204 B/op 29 allocs/op
PASS
ok golearn/tool_test 50.387sSau đó sử dụng benchstat để so sánh
$ benchstat old.txt new.txt
goos: windows
goarch: amd64
pkg: golearn/tool_test
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
│ old.txt │ new.txt │
│ sec/op │ sec/op vs base │
ConcatDirect-2 299.0m ± ∞ ¹ 286.7m ± ∞ ¹ ~ (p=0.310 n=5)
ConcatDirect-4 894.7m ± ∞ ¹ 1123.2m ± ∞ ¹ +25.53% (p=0.008 n=5)
ConcatDirect-8 684.0m ± ∞ ¹ 706.6m ± ∞ ¹ ~ (p=0.548 n=5)
ConcatBuilder-2 423.9µ ± ∞ ¹ 339.0µ ± ∞ ¹ -20.04% (p=0.008 n=5)
ConcatBuilder-4 1068.7µ ± ∞ ¹ 993.8µ ± ∞ ¹ ~ (p=0.151 n=5)
ConcatBuilder-8 736.5µ ± ∞ ¹ 750.5µ ± ∞ ¹ ~ (p=0.841 n=5)
geomean 19.84m 19.65m -0.98%
¹ need >= 6 samples for confidence interval at level 0.95
│ old.txt │ new.txt │
│ B/op │ B/op vs base │
ConcatDirect-2 3.763Gi ± ∞ ¹ 3.763Gi ± ∞ ¹ ~ (p=1.000 n=5)
ConcatDirect-4 3.763Gi ± ∞ ¹ 3.763Gi ± ∞ ¹ -0.00% (p=0.008 n=5)
ConcatDirect-8 3.763Gi ± ∞ ¹ 3.763Gi ± ∞ ¹ -0.00% (p=0.008 n=5)
ConcatBuilder-2 3.937Mi ± ∞ ¹ 3.937Mi ± ∞ ¹ ~ (p=1.000 n=5) ²
ConcatBuilder-4 3.937Mi ± ∞ ¹ 3.937Mi ± ∞ ¹ ~ (p=0.079 n=5)
ConcatBuilder-8 3.937Mi ± ∞ ¹ 3.937Mi ± ∞ ¹ ~ (p=0.952 n=5)
geomean 123.2Mi 123.2Mi -0.00%
¹ need >= 6 samples for confidence interval at level 0.95
² all samples are equal
│ old.txt │ new.txt │
│ allocs/op │ allocs/op vs base │
ConcatDirect-2 9.999k ± ∞ ¹ 9.999k ± ∞ ¹ ~ (p=1.000 n=5)
ConcatDirect-4 10.22k ± ∞ ¹ 10.14k ± ∞ ¹ -0.74% (p=0.008 n=5)
ConcatDirect-8 10.35k ± ∞ ¹ 10.22k ± ∞ ¹ -1.26% (p=0.008 n=5)
ConcatBuilder-2 29.00 ± ∞ ¹ 29.00 ± ∞ ¹ ~ (p=1.000 n=5) ²
ConcatBuilder-4 29.00 ± ∞ ¹ 29.00 ± ∞ ¹ ~ (p=1.000 n=5) ²
ConcatBuilder-8 29.00 ± ∞ ¹ 29.00 ± ∞ ¹ ~ (p=1.000 n=5) ²
geomean 543.6 541.7 -0.33%
¹ need >= 6 samples for confidence interval at level 0.95
² all samples are equalTừ kết quả có thể thấy benchstat chia thành ba nhóm lần lượt là thời gian tiêu thụ mức sử dụng bộ nhớ và số lần phân bổ bộ nhớ trong đó geomean là giá trị trung bình p là mức ý nghĩa thống kê của mẫu khoảng cách tới hạn thường là 0.05 cao hơn 0.05 thì không quá đáng tin lấy một dữ liệu trong đó như sau
│ sec/op │ sec/op vs base │
ConcatDirect-4 894.7m ± ∞ ¹ 1123.2m ± ∞ ¹ +25.53% (p=0.008 n=5)Có thể thấy thời gian thực thi của old là 894.7ms thời gian thực thi của new là 1123.2ms so với đó còn tăng 25.53% thời gian tiêu thụ.
Kiểm thử fuzzing
Kiểm thử fuzzing là một tính năng mới được ra mắt trong GO1.18 thuộc là một loại tăng cường của kiểm thử đơn vị và kiểm thử benchmark điểm khác biệt là dữ liệu kiểm thử của hai cái trước đều cần nhà phát triển thủ công viết còn kiểm thử fuzzing có thể thông qua corpus để tạo dữ liệu kiểm thử ngẫu nhiên về kiểm thử fuzzing trong Go có thể đến Go Fuzzing để biết thêm khái niệm. Lợi ích của kiểm thử fuzzing là so với dữ liệu kiểm thử cố định dữ liệu ngẫu nhiên có thể kiểm tra điều kiện biên của chương trình tốt hơn. Dưới đây lấy ví dụ của hướng dẫn chính thức để giải thích lần này cần kiểm tra là một hàm đảo ngược chuỗi trước tiên tạo file /tool/strings.go ghi mã như sau
package tool
func Reverse(s string) string {
b := []byte(s)
for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}Tạo file kiểm thử fuzzing /tool_test/fuzz_tool_test.go ghi mã như sau
package tool
import (
"golearn/tool"
"testing"
"unicode/utf8"
)
func FuzzReverse(f *testing.F) {
testdata := []string{"hello world!", "nice to meet you", "good bye!"}
for _, data := range testdata {
f.Add(data)
}
f.Fuzz(func(t *testing.T, str string) {
first := tool.Reverse(str)
second := tool.Reverse(first)
t.Logf("str:%q,first:%q,second:%q", str, first, second)
if str != second {
t.Errorf("before: %q, after: %q", str, second)
}
if utf8.ValidString(str) && !utf8.ValidString(first) {
t.Errorf("Reverse produced invalid UTF-8 string %q %q", str, first)
}
})
}Trong kiểm thử fuzzing trước tiên cần thêm dữ liệu vào corpus seed library ví dụ sử dụng f.Add() để thêm giúp tạo dữ liệu kiểm thử ngẫu nhiên tiếp theo. Sau đó sử dụng f.Fuzz(fn) để kiểm tra chữ ký hàm như sau
func (f *F) Fuzz(ff any)
func (f *F) Add(args ...any)fn giống như logic của hàm kiểm thử đơn vị tham số đầu vào của hàm đầu tiên phải là t *testing.T sau đó là các tham số muốn tạo. Vì chuỗi truyền vào không thể đoán trước ở đây sử dụng phương pháp đảo ngược hai lần để xác minh. Thực hiện lệnh sau
$ go test -run Fuzz -v
=== RUN FuzzReverse
=== RUN FuzzReverse/seed#0
fuzz_tool_test.go:18: str:"hello world!",first:"!dlrow olleh",second:"hello world!"
=== RUN FuzzReverse/seed#1
fuzz_tool_test.go:18: str:"nice to meet you",first:"uoy teem ot ecin",second:"nice to meet you"
=== RUN FuzzReverse/seed#2
fuzz_tool_test.go:18: str:"good bye!",first:"!eyb doog",second:"good bye!"
--- PASS: FuzzReverse (0.00s)
--- PASS: FuzzReverse/seed#0 (0.00s)
--- PASS: FuzzReverse/seed#1 (0.00s)
--- PASS: FuzzReverse/seed#2 (0.00s)
PASS
ok golearn/tool_test 0.539sKhi tham số không mang -fuzz sẽ không tạo dữ liệu kiểm thử ngẫu nhiên chỉ truyền dữ liệu trong corpus vào hàm kiểm thử có thể thấy từ kết quả kiểm thử tất cả đều vượt qua sử dụng như vậy tương đương với kiểm thử đơn vị nhưng thực ra có vấn đề dưới đây thêm tham số -fuzz thực hiện lại.
$ go test -fuzz . -fuzztime 30s -run Fuzz -v
=== RUN FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/217 completed
fuzz: minimizing 91-byte failing input file
fuzz: elapsed: 0s, gathering baseline coverage: 15/217 completed
--- FAIL: FuzzReverse (0.13s)
--- FAIL: FuzzReverse (0.00s)
fuzz_tool_test.go:18: str:"𐑄",first:"\x84\x91\x90\xf0",second:"𐑄"
fuzz_tool_test.go:23: Reverse produced invalid UTF-8 string "𐑄" "\x84\x91\x90\xf0"
Failing input written to testdata\fuzz\FuzzReverse\d856c981b6266ba2
To re-run:
go test -run=FuzzReverse/d856c981b6266ba2
=== NAME
FAIL
exit status 1
FAIL golearn/tool_test 0.697sTIP
Ca kiểm thử thất bại trong kiểm thử fuzzing sẽ xuất vào một file corpus nào đó dưới thư mục testdata của thư mục kiểm thử hiện tại ví dụ trong ví dụ trên
Failing input written to testdata\fuzz\FuzzReverse\d856c981b6266ba2
To re-run:
go test -run=FuzzReverse/d856c981b6266ba2testdata\fuzz\FuzzReverse\d856c981b6266ba2 là đường dẫn file corpus xuất nội dung file như sau
go test fuzz v1
string("𐑄")Có thể thấy lần này không vượt qua lý do là chuỗi đảo ngược sau trở thành định dạng không phải utf8 nên thông qua kiểm thử fuzzing đã phát hiện vấn đề này. Vì một số ký tự chiếm không chỉ một byte nếu lấy đơn vị byte đảo ngược chắc chắn là mã lộn xộn nên sửa mã nguồn chờ kiểm tra thành như sau chuyển chuỗi thành []rune như vậy có thể tránh xuất hiện vấn đề trên.
func Reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}Tiếp theo trực tiếp chạy ca kiểm thử thất bại của kiểm thử fuzzing lần trước
$ go test -run=FuzzReverse/d856c981b6266ba2 -v
=== RUN FuzzReverse
=== RUN FuzzReverse/d856c981b6266ba2
fuzz_tool_test.go:18: str:"𐑄",first:"𐑄",second:"𐑄"
--- PASS: FuzzReverse (0.00s)
--- PASS: FuzzReverse/d856c981b6266ba2 (0.00s)
PASS
ok golearn/tool_test 0.033sCó thể thấy lần này vượt qua kiểm thử thực hiện lại kiểm thử fuzzing xem còn vấn đề gì không
$ go test -fuzz . -fuzztime 30s -run Fuzz -v
=== RUN FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/219 completed
fuzz: minimizing 70-byte failing input file
failure while testing seed corpus entry: FuzzReverse/d97214ce235bfcf5
fuzz: elapsed: 0s, gathering baseline coverage: 2/219 completed
--- FAIL: FuzzReverse (0.15s)
--- FAIL: FuzzReverse (0.00s)
fuzz_tool_test.go:18: str:"\xe4",first:"",second:""
fuzz_tool_test.go:20: before: "\xe4", after: ""
=== NAME
FAIL
exit status 1
FAIL golearn/tool_test 0.184sCó thể thấy lại xảy ra lỗi lần này vấn đề là sau khi đảo ngược chuỗi hai lần không bằng nhau ký tự gốc là \xe4 kết quả mong đợi là 4ex\ nhưng kết quả là mã lộn xộn như sau
func main() {
fmt.Println("\xe4")
fmt.Println([]byte("\xe4"))
fmt.Println([]rune("\xe4"))
fmt.Printf("%q\n", "\xe4")
fmt.Printf("%x\n", "\xe4")
}Kết quả thực hiện của nó là
[65533]
"\xe4"
e4Nguyên nhân究 cùng là vì \xe4 đại diện cho một byte nhưng không phải là một chuỗi UTF-8 hợp lệ (trong mã hóa UTF-8 \xe4 là bắt đầu của ký tự ba byte nhưng thiếu hai byte phía sau). Khi chuyển thành []rune Golang tự động biến nó thành []rune chứa单个 ký tự Unicode []rune{"\uFFFD"} sau khi đảo ngược vẫn là []rune{"\uFFFD"} khi chuyển lại thành string ký tự Unicode này lại được thay thế bằng mã hóa UTF-8 của nó \xef\xbf\xbd. Do đó một cách giải quyết là nếu truyền vào là chuỗi không phải utf8 trực tiếp trả về lỗi
func Reverse(s string) (string, error) {
if !utf8.ValidString(s) {
return s, errors.New("input is not valid UTF-8")
}
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r), nil
}Mã kiểm thử cũng cần sửa một chút
func FuzzReverse(f *testing.F) {
testdata := []string{"hello world!", "nice to meet you", "good bye!"}
for _, data := range testdata {
f.Add(data)
}
f.Fuzz(func(t *testing.T, str string) {
first, err := tool.Reverse(str)
if err != nil {
t.Skip()
}
second, err := tool.Reverse(first)
if err != nil {
t.Skip()
}
t.Logf("str:%q,first:%q,second:%q", str, first, second)
if str != second {
t.Errorf("before: %q, after: %q", str, second)
}
if utf8.ValidString(str) && !utf8.ValidString(first) {
t.Errorf("Reverse produced invalid UTF-8 string %q %q", str, first)
}
})
}Khi hàm đảo ngược trả về error thì bỏ qua kiểm thử lại thực hiện kiểm thử fuzzing
$ go test -fuzz . -fuzztime 30s -run Fuzz -v
=== RUN FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/219 completed
fuzz: elapsed: 0s, gathering baseline coverage: 219/219 completed, now fuzzing with 16 workers
fuzz: elapsed: 3s, execs: 895571 (297796/sec), new interesting: 32 (total: 251)
fuzz: elapsed: 6s, execs: 1985543 (363120/sec), new interesting: 37 (total: 256)
fuzz: elapsed: 9s, execs: 3087837 (367225/sec), new interesting: 38 (total: 257)
fuzz: elapsed: 12s, execs: 4090817 (335167/sec), new interesting: 40 (total: 259)
fuzz: elapsed: 15s, execs: 5132580 (346408/sec), new interesting: 44 (total: 263)
fuzz: elapsed: 18s, execs: 6248486 (372185/sec), new interesting: 45 (total: 264)
fuzz: elapsed: 21s, execs: 7366827 (373305/sec), new interesting: 46 (total: 265)
fuzz: elapsed: 24s, execs: 8439803 (358059/sec), new interesting: 47 (total: 266)
fuzz: elapsed: 27s, execs: 9527671 (361408/sec), new interesting: 47 (total: 266)
fuzz: elapsed: 30s, execs: 10569473 (348056/sec), new interesting: 48 (total: 267)
fuzz: elapsed: 30s, execs: 10569473 (0/sec), new interesting: 48 (total: 267)
--- PASS: FuzzReverse (30.16s)
=== NAME
PASS
ok golearn/tool_test 30.789sSau đó lần này có thể nhận được log xuất của kiểm thử fuzzing khá hoàn chỉnh trong đó giải thích một số khái niệm như sau
- elapsed: thời gian đã trôi qua sau khi hoàn thành một vòng
- execs: tổng số đầu vào đã chạy, 297796/sec biểu thị bao nhiêu đầu vào mỗi giây
- new interesting: tổng số đầu vào "thú vị" đã thêm vào corpus trong kiểm thử. (đầu vào thú vị chỉ đầu vào có thể mở rộng phạm vi phủ mã đến ngoài phạm vi mà corpus hiện tại có thể phủ随着覆盖范围的不断扩大,它的增长趋势总体上而言会持续变缓)
TIP
如果没有 -fuzztime 参数限制时间,模糊测试将会永远的运行下去。
Hỗ trợ kiểu dữ liệu
Go Fuzz hỗ trợ các kiểu dữ liệu sau:
string,[]byteint,int8,int16,int32/rune,int64uint,uint8/byte,uint16,uint32,uint64float32,float64bool
