Skip to content

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.go ví dụ muốn kiểm thử một chức năng nào đó thì đặt tên nó là function_test.go nế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.go hoặc example_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à ExampleXXXX kiểm thử đơn vị là TestXXXX kiểm thử benchmark là BenchmarkXXXX kiểm thử fuzzing là FuzzXXXX như 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

go
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

go
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

sh
$ 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.

sh
$ 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, -v

Nế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.

sh
$ go test example_test.go
ok      command-line-arguments  0.457s

Hoặ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ụ

sh
$ go test -run ExampleSay
PASS
ok      golearn/test    0.038s

Ba 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ụ

sh
$ 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.040s

Như 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

sh
$ 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

sh
$ 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 fileChỉ định tên file nhị phân sau khi biên dịch
-cChỉ biên dịch file kiểm thử nhưng không chạy
-jsonXuất log kiểm thử dưới dạng json
-exec xprogSử dụng xprog để chạy kiểm thử tương đương với go run
-bench regexpChọn kiểm thử benchmark khớp với regexp
-fuzz regexpChọn kiểm thử fuzzing khớp với regexp
-fuzztime tThờ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 tThời gian chạy tối thiểu của kiểm thử mode quy tắc như trên
-count nChạy kiểm thử n lần mặc định 1 lần
-coverBật phân tích độ phủ kiểm thử
-covermode set,count,atomicĐặt mode phân tích độ phủ
-cpuThực hiện GOMAXPROCS cho thực thi kiểm thử
-failfastSau khi kiểm thử thất bại lần đầu sẽ không bắt đầu kiểm thử mới
-list regexpLiệt kê ca kiểm thử khớp với regexp
-parallel nCho 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 regexpChỉ chạy ca kiểm thử khớp với regexp
-skip regexpBỏ qua ca kiểm thử khớp với regexp
-timeout dNế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,NXá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
-vXuất log kiểm thử chi tiết hơn
-benchmemThống kê phân bổ bộ nhớ của kiểm thử benchmark
-blockprofile block.outThống kê tình hình block goroutine trong kiểm thử và ghi vào file
-blockprofilerate nKiể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.outThống kê tình hình kiểm thử độ phủ và ghi vào file
-cpuprofile cpu.outThống kê tình hình cpu và ghi vào file
-memprofile mem.outThống kê tình hình phân bổ bộ nhớ và ghi vào file
-memprofilerate nKiể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.outThố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.outGhi tình hình theo dõi thực thi vào file
-outputdir directoryChỉ đị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

go
// 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

go
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

go
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ả.

sh
$ 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.448s

Từ 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:
// hello

Cách viết thứ hai là nhiều dòng xuất tức kiểm tra khớp theo thứ tự

// Output:
// hello
// bye

Cá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
// hello

Cầ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

go
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.

go
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

go
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

sh
$ 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.037s

Từ 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.

go
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
$ 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.468s

Ví 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.

go
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.462s

Helper

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.

go
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.464s

TIP

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

go
// 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)) bool

Dưới đây là một ví dụ

go
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

sh
$ 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.449s

Thô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.

go
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.444s

Từ 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

go
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.450s

Phong 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

go
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

go
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.

go
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

go
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

sh
$ 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.381s

Dướ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

sh
$ go install golang.org/x/perf/benchstat

Thự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.txtnew.txt để so sánh kết quả thực hiện lần đầu

sh
$ 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.742s

Kết quả thực hiện lần hai

sh
$ 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.387s

Sau đó sử dụng benchstat để so sánh

sh
$ 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 equal

Từ 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

go
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

go
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

go
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

sh
$ 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.539s

Khi 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.

sh
$ 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.697s

TIP

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/d856c981b6266ba2

testdata\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.

go
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

sh
$ 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.033s

Có 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

sh
$ 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.184s

Có 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

go
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"
e4

Nguyê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

go
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

go
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

sh
$ 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.789s

Sau đó 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, []byte
  • int, int8, int16, int32/rune, int64
  • uint, uint8/byte, uint16, uint32, uint64
  • float32, float64
  • bool

Golang by www.golangdev.cn edit