Skip to content

Testing

Bagi developer, testing yang baik dapat menemukan error dalam program lebih awal, menghindari beban mental yang disebabkan oleh Bug karena tidak tepat waktu maintenance, jadi menulis testing dengan baik sangat perlu. Go menyediakan tool command line go test yang sangat mudah digunakan dan praktis dalam aspek testing, dapat melihat testing di standard library dan banyak framework open source, tool ini sangat mudah digunakan, saat ini mendukung beberapa jenis testing berikut:

  • Example testing
  • Unit testing
  • Benchmark testing
  • Fuzz testing

Sebagian besar API di Go disediakan oleh paket standar testing.

TIP

Jalankan command go help testfunc di command line, dapat melihat penjelasan官方 Go untuk empat jenis testing di atas.

Sebelum mulai menulis testing, pertama perlu memperhatikan beberapa spesifikasi, ini akan lebih mudah untuk pembelajaran selanjutnya.

  • Paket testing: File testing sebaiknya terpisah di paket sendiri, paket ini biasanya dinamai test.
  • File testing: File testing biasanya berakhiran _test.go, misalnya jika ingin menguji suatu fungsi, namai function_test.go, jika ingin membagi lebih detail menurut jenis testing juga dapat menggunakan jenis testing sebagai prefix file, misalnya benchmark_marshaling_test.go, atau example_marshaling_test.go.
  • Fungsi testing: Setiap file testing akan memiliki beberapa fungsi testing untuk testing yang berbeda. Untuk jenis testing yang berbeda, gaya penamaan fungsi testing juga berbeda. Misalnya example testing adalah ExampleXXXX, unit testing adalah TestXXXX, benchmark testing adalah BenchmarkXXXX, fuzz testing adalah FuzzXXXX, dengan demikian meskipun tanpa komentar juga dapat mengetahui ini adalah jenis testing apa.

TIP

Saat nama paket adalah testdata, paket ini biasanya untuk menyimpan data auxiliary untuk testing, saat menjalankan testing, Go akan mengabaikan paket bernama testdata.

Mengikuti spesifikasi di atas, membiasakan gaya testing yang baik, dapat menghemat banyak trouble untuk maintenance di masa depan.

Menjalankan Testing

Menjalankan testing terutama menggunakan command go test, berikut menggunakan kode aktual sebagai contoh, sekarang ada file yang akan diuji /say/hello.go kode sebagai berikut

go
package say

import "fmt"

func Hello() {
  fmt.Println("hello")
}

func GoodBye() {
  fmt.Println("bye")
}

Dan file testing /test/example_test.go kode sebagai berikut

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
}

Ada beberapa cara untuk menjalankan testing ini, misalnya ingin menjalankan semua test case di paket test, dapat langsung jalankan command berikut di direktori test

sh
$ go test ./
PASS
ok      golearn/test    0.422s

./表示 direktori saat ini, Go akan mengkompilasi ulang semua file testing di direktori test, lalu menjalankan semua test case, dari hasil dapat dilihat semua test case lulus. Parameter setelahnya juga dapat mengikuti beberapa direktori, misalnya command di bawah, jelas direktori utama proyek tidak ada file testing untuk dijalankan.

sh
$ go test ./ ../
ok      golearn/test
?       golearn [no test files]

TIP

Saat parameter yang dijalankan adalah beberapa paket, Go tidak akan menjalankan ulang test case yang sudah berhasil lulus, saat menjalankan akan menambahkan (cached) di akhir baris untuk表示 output hasil adalah cache dari sebelumnya. Saat flag parameter testing berada di集合 berikut, Go akan cache hasil testing, jika tidak maka tidak.

-benchtime, -cpu,-list, -parallel, -run, -short, -timeout, -failfast, -v

Jika ingin menonaktifkan cache, dapat menambahkan parameter -count=1.

Tentu juga dapat menentukan file testing tertentu untuk dijalankan.

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

Atau dapat menentukan test case tertentu dari file testing tertentu, misalnya

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

Ketiga situasi di atas meskipun menyelesaikan testing, tetapi hasil output terlalu ringkas, saat ini dapat menambahkan parameter -v, untuk membuat hasil output lebih detail, misalnya

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

Sekarang dapat dengan jelas melihat urutan eksekusi setiap test case, waktu yang dikonsumsi, situasi eksekusi, dan total waktu yang dikonsumsi.

TIP

Command go test secara default menjalankan semua unit testing, example testing, fuzz testing, jika menambahkan parameter -bench maka akan menjalankan semua jenis testing, misalnya command di bawah

sh
$ go test -bench .

Jadi perlu menggunakan parameter -run untuk menentukan, misalnya hanya menjalankan semua benchmark testing command sebagai berikut

sh
$ go test -bench . -run ^$

Parameter Umum

Go testing memiliki sangat banyak flag parameter, di bawah hanya akan memperkenalkan parameter yang umum digunakan, ingin了解 lebih banyak detail disarankan menggunakan command go help testflag untuk查阅 sendiri.

ParameterPenjelasan
-o fileMenentukan nama file biner setelah kompilasi
-cHanya mengkompilasi file testing, tetapi tidak menjalankan
-jsonOutput log testing dalam format json
-exec xprogMenjalankan testing menggunakan xprog, setara dengan go run
-bench regexpMemilih benchmark testing yang cocok dengan regexp
-fuzz regexpMemilih fuzz testing yang cocok dengan regexp
-fuzztime tWaktu otomatis结束 fuzz testing, t adalah interval waktu, saat satuan x,表示 jumlah, misalnya 200x
-fuzzminimizetime tWaktu minimum mode testing berjalan, aturan sama di atas
-count nMenjalankan testing n kali, default 1 kali
-coverMengaktifkan analisis coverage testing
-covermode set,count,atomicMenset mode analisis coverage
-cpuUntuk testing menjalankan GOMAXPROCS
-failfastSetelah testing gagal pertama kali, tidak akan memulai testing baru
-list regexpMendaftar test case yang cocok dengan regexp
-parallel nMengizinkan test case yang memanggil t.Parallel berjalan paralel, n adalah jumlah maksimum paralel
-run regexpHanya menjalankan test case yang cocok dengan regexp
-skip regexpMelewati test case yang cocok dengan regexp
-timeout dJika waktu eksekusi single testing melebihi interval waktu d, akan panic. d adalah interval waktu, misalnya 1s,1ms,1ns dll
-shuffle off,on,NMengacak urutan eksekusi testing, N adalah seed random, default seed adalah waktu sistem
-vOutput log testing lebih detail
-benchmemStatistik alokasi memori benchmark testing
-blockprofile block.outStatistik situasi blocking goroutine dalam testing dan menulis ke file
-blockprofilerate nMengontrol frekuensi statistik blocking goroutine, melalui command go doc runtime.SetBlockProfileRate lihat lebih banyak detail
-coverprofile cover.outStatistik situasi coverage testing dan menulis ke file
-cpuprofile cpu.outStatistik situasi cpu dan menulis ke file
-memprofile mem.outStatistik situasi alokasi memori dan menulis ke file
-memprofilerate nMengontrol frekuensi statistik alokasi memori, melalui command go doc runtime.MemProfileRate lihat lebih banyak detail
-mutexprofile mutex.outStatistik situasi lock contention dan menulis ke file
-mutexprofilefraction nMenset statistik n goroutine contend satu mutex lock
-trace trace.outMenulis situasi execution tracking ke file
-outputdir directoryMenentukan direktori output file statistik di atas, default adalah direktori running go test

Example Testing

Example testing tidak seperti tiga jenis testing lainnya untuk menemukan masalah program, ia lebih untuk menampilkan cara penggunaan suatu fungsi, berperan sebagai dokumentasi. Example testing bukan konsep yang didefinisikan官方, juga bukan spesifikasi yang keras, lebih seperti konvensi dalam engineering, apakah mengikuti hanya tergantung pada developer. Example testing muncul sangat banyak di standard library, biasanya adalah contoh kode standard library yang ditulis官方, misalnya fungsi testing ExampleWithDeadline di context/example_test.go standard library, fungsi ini menampilkan cara penggunaan dasar 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
}

Secara permukaan fungsi testing ini adalah fungsi biasa, tetapi example testing terutama tercermin dari komentar Output, fungsi yang akan diuji hanya memiliki satu baris output, menggunakan komentar Output untuk mendeteksi output. Pertama buat file bernama hello.go, tulis kode sebagai berikut

go
package say

import "fmt"

func Hello() {
  fmt.Println("hello")
}

func GoodBye() {
  fmt.Println("bye")
}

Fungsi SayHello adalah fungsi yang akan diuji, lalu buat file testing example_test.go, tulis kode sebagai berikut

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
}

Komentar Output di fungsi menunjukkan mendeteksi output fungsi apakah hello, selanjutnya jalankan command testing lihat hasilnya.

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

Dari hasil dapat dilihat semua testing sudah lulus, tentang Output ada beberapa cara penulisan, pertama adalah hanya satu baris output, berarti mendeteksi output fungsi ini apakah hello

// Output:
// hello

Kedua adalah multi-baris output, yaitu mendeteksi output apakah cocok sesuai urutan

// Output:
// hello
// bye

Ketiga adalah unordered output, yaitu tidak按照 urutan multi-baris output matching

// Unordered output:
// bye
// hello

Perlu diperhatikan, untuk fungsi testing而言, hanya ketika beberapa baris terakhir adalah komentar Output akan dianggap sebagai example testing, jika tidak hanya fungsi biasa, tidak akan dijalankan oleh Go.

Unit Testing

Unit testing adalah testing terhadap unit testable terkecil dalam software, ukuran unit didefinisikan oleh developer, mungkin adalah struct, atau paket, juga mungkin adalah fungsi, atau tipe. Berikut masih melalui contoh untuk mendemonstrasikan, pertama buat file /tool/math.go, tulis kode sebagai berikut

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
}

Lalu buat file testing /tool_test/unit_test.go, untuk unit testing而言, penamaan dapat menggunakan unit_test atau paket yang ingin diuji atau fungsi sebagai prefix 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)
  }
}

Untuk unit testing而言, gaya penamaan setiap test case adalah TestXXXX, dan parameter input fungsi harus t *testing.T, testing.T adalah struct yang disediakan paket testing untuk memudahkan testing, menyediakan banyak method yang dapat digunakan, t.Errorf di contoh setara dengan t.Logf, untuk output informasi log testing yang gagal dengan format, lainnya yang umum digunakan还有 t.Fail untuk menandai test case saat ini sebagai testing gagal, fungsi serupa还有 t.FailNow juga akan menandai sebagai testing gagal, tetapi yang前者 setelah gagal masih akan继续 menjalankan, yang后者 akan langsung berhenti menjalankan, seperti contoh di bawah, modifikasi hasil expected menjadi hasil yang salah:

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 internal menggunakan t.Fail()
    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 internal menggunakan t.FailNow()
    t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
  }
  t.Log("test finished")
}

Jalankan testing di atas output sebagai berikut

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

Dari log testing dapat dilihat TestSum meskipun gagal masih output test finished, sedangkan TestEqual tidak, sama还有 t.SkipNow, akan menandai test case saat ini sebagai SKIP, lalu berhenti menjalankan, di testing putaran selanjutnya akan继续 menjalankan.

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")
}

Saat menjalankan testing, modifikasi jumlah testing menjadi 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

Contoh di数 output test finished di baris terakhir, untuk表示 testing selesai, sebenarnya dapat menggunakan t.Cleanup untuk mendaftarkan fungsi cleanup khusus melakukan hal ini, fungsi ini akan dijalankan saat test case结束, sebagai berikut.

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)
  }
}

Setelah menjalankan testing output sebagai berikut

$ 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

Melalui t.Helper() dapat menandai fungsi saat ini sebagai helper function, helper function tidak akan单独 sebagai test case untuk dijalankan, saat mencatat log nomor baris yang output juga adalah nomor baris caller helper function, ini dapat membuat analisis log lebih akurat定位, menghindari informasi冗杂 lainnya. Misalnya contoh t.Cleanup di atas dapat dimodifikasi menjadi helper function, sebagai berikut.

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)
   }
}

Setelah menjalankan testing output informasi sebagai berikut, perbedaan dengan sebelumnya adalah nomor baris test finished menjadi nomor baris caller.

$ 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

Operasi di atas hanya dapat dilakukan di main testing, yaitu test case yang langsung dijalankan, jika digunakan di sub testing akan panic.

Sub Testing

Dalam beberapa situasi, akan perlu menguji test case lain di dalam satu test case, test case nested seperti ini umumnya disebut sub testing, melalui method t.Run(), signature method sebagai berikut

go
// Method Run akan开启 goroutine baru untuk menjalankan sub testing, blocking menunggu fungsi f selesai baru akan return
// Nilai return adalah apakah testing lulus
func (t *T) Run(name string, f func(t *T)) bool

Berikut adalah contoh

go
func TestTool(t *testing.T) {
  t.Run("tool.Sum(10,101)", TestSum)
  t.Run("tool.Equal(10,101)", TestEqual)
}

Setelah menjalankan hasil sebagai berikut

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

Melalui output dapat dengan jelas melihat struktur hierarki parent-child, di contoh di atas sub testing pertama belum selesai sub testing kedua tidak akan dijalankan, dapat menggunakan t.Parallel() untuk menandai test case dapat berjalan paralel, dengan demikian urutan output akan tidak dapat ditentukan.

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")
}

Setelah menjalankan testing output sebagai berikut

$ 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

Dari hasil testing dapat dengan jelas melihat ada proses blocking menunggu, saat menjalankan test case secara konkuren, seperti contoh di atas jelas tidak dapat berjalan normal, karena kode selanjutnya tidak dapat menjamin sinkron berjalan, saat ini dapat memilih nested satu lapis t.Run() lagi, sebagai berikut

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")
}

Jalankan lagi, dapat melihat hasil eksekusi yang normal.

$ 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

Gaya Tabel

Di unit testing di atas, data input testing semuanya adalah variabel yang dideklarasikan manual, saat ukuran data kecil tidak masalah, tetapi jika ingin menguji banyak grup data, tidak mungkin lagi mendeklarasikan variabel untuk membuat data testing, jadi umumnya sebisa mungkin menggunakan slice struct, struct adalah struct anonim yang dideklarasikan sementara, karena gaya coding seperti ini terlihat seperti tabel, jadi disebut table-driven. Berikut contoh, ini adalah contoh mendeklarasikan beberapa variabel manual untuk membuat data testing, jika ada banyak grup data terlihat tidak terlalu intuitif, jadi dimodifikasi menjadi gaya tabel

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)
  }
}

Kode setelah dimodifikasi sebagai berikut

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)
    }
  }
}

Data testing seperti ini terlihat lebih intuitif.

Benchmark Testing

Benchmark testing juga disebut performance testing, biasanya用于 testing okupansi memori program, situasi penggunaan CPU, waktu eksekusi dll indikator performa. Untuk benchmark testing而言, file testing biasanya berakhiran bench_test.go, dan nama fungsi test case harus format BenchmarkXXXX.

Berikut menggunakan contoh performa string concatenation sebagai contoh benchmark testing. Pertama buat file /tool/strConcat.go, seperti diketahui langsung menggunakan string进行 + concatenation performa sangat rendah, sedangkan menggunakan strings.Builder jauh lebih baik, di file /tool/strings.go masing-masing buat dua fungsi untuk dua cara string concatenation.

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)
   }
}

Lalu buat file testing /tool_test/bench_tool_test.go , kode sebagai berikut

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)
  }
}

Jalankan command testing, command mengaktifkan log detail dan analisis memori, menentukan jumlah core CPU yang digunakan, dan setiap test case dijalankan dua putaran, output sebagai berikut

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

Di bawah menjelaskan hasil output benchmark testing, goos mewakili sistem operasi yang dijalankan, goarh mewakili arsitektur CPU, pkg adalah paket tempat testing berada, cpu adalah beberapa informasi tentang CPU. Hasil setiap test case dipisahkan oleh nama setiap benchmark testing, kolom pertama BenchmarkConcatDirect-2 angka 2 mewakili jumlah core CPU yang digunakan, kolom kedua 4 mewakili ukuran b.N di kode, yaitu jumlah loop di benchmark testing, kolom ketiga 277771375 ns/op mewakili waktu yang dikonsumsi setiap loop, ns adalah nanodetik, kolom keempat 4040056736 B/op mewakili ukuran byte memori yang dialokasikan setiap loop, kolom kelima 10000 allocs/op mewakili jumlah alokasi memori setiap loop.

Jelas, dari hasil testing, performa menggunakan strings.Builder jauh lebih tinggi daripada menggunakan + untuk concatenation string, melalui perbandingan data yang intuitif这正是 tujuan benchmark testing.

benchstat

benchstat adalah tool analisis performa testing open source, sampel testing performa di atas hanya dua grup, sekali sampel banyak起来 analisis manual akan sangat memakan waktu dan tenaga, tool ini lahir untuk menyelesaikan masalah analisis performa.

Pertama perlu download tool ini

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

Jalankan benchmark testing dua kali, kali ini modifikasi jumlah sampel menjadi 5, dan masing-masing output ke file old.txt dan new.txt untuk perbandingan, hasil eksekusi pertama

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

Hasil eksekusi kedua

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

Lalu gunakan benchstat untuk perbandingan

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

Dari hasil dapat dilihat benchstat membaginya menjadi tiga grup, masing-masing adalah waktu, okupansi memori dan jumlah alokasi memori, di antaranya geomean adalah nilai rata-rata, p adalah tingkat signifikansi sampel, interval kritis biasanya 0.05, di atas 0.05 tidak terlalu可信, ambil satu data di antaranya sebagai berikut:

          │    sec/op     │    sec/op      vs base               │
ConcatDirect-4     894.7m ± ∞ ¹   1123.2m ± ∞ ¹  +25.53% (p=0.008 n=5)

Dapat melihat waktu eksekusi old adalah 894.7ms, waktu eksekusi new 1123.2ms, dibandingkan masih增加了 25.53% waktu.

Fuzz Testing

Fuzz testing adalah fitur baru yang diluncurkan GO1.18, termasuk增强 dari unit testing dan benchmark testing, perbedaannya adalah data testing keduanya perlu developer tulis manual, sedangkan fuzz testing dapat menghasilkan data testing random melalui corpus, tentang fuzz testing di Go dapat前往 Go Fuzzing untuk了解 lebih banyak konsep. Keuntungan fuzz testing adalah, dibandingkan dengan data testing tetap, data random dapat lebih baik menguji kondisi batas program. Berikut menggunakan contoh tutorial官方 untuk menjelaskan, kali ini perlu menguji fungsi reverse string, pertama buat file /tool/strings.go, tulis kode sebagai berikut

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)
}

Buat file fuzz testing /tool_test/fuzz_tool_test.go, tulis kode sebagai berikut

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)
    }
  })
}

Di fuzz testing, pertama perlu menambahkan data ke seed corpus, contoh menggunakan f.Add() untuk menambahkan, membantu menghasilkan data testing random selanjutnya. Lalu menggunakan f.Fuzz(fn) untuk testing, signature fungsi sebagai berikut:

go
func (f *F) Fuzz(ff any)

func (f *F) Add(args ...any)

fn mirip seperti logika fungsi unit testing, parameter input pertama harus t *testing.T, diikuti parameter yang ingin dihasilkan. Karena string yang传入 tidak dapat diprediksi, di sini menggunakan metode reverse dua kali untuk verifikasi. Jalankan command berikut

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

Saat parameter tidak带 -fuzz, tidak akan menghasilkan data testing random, hanya akan传入 data di corpus ke fungsi testing, dari hasil dapat dilihat semua testing lulus, menggunakan seperti ini setara dengan unit testing, tetapi sebenarnya ada masalah, di bawah tambahkan parameter -fuzz jalankan lagi.

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

Test case yang gagal di fuzz testing akan output ke file corpus tertentu di direktori testdata di folder testing saat ini, misalnya contoh di atas

Failing input written to testdata\fuzz\FuzzReverse\d856c981b6266ba2
To re-run:
go test -run=FuzzReverse/d856c981b6266ba2

testdata\fuzz\FuzzReverse\d856c981b6266ba2 adalah path file corpus yang output, konten file sebagai berikut

go test fuzz v1
string("𐑄")

Dapat dilihat kali ini tidak lulus, alasannya adalah string setelah reverse menjadi format non-utf8, jadi melalui fuzz testing menemukan masalah ini. Karena beberapa karakter menempati tidak hanya satu byte, jika reverse dengan satuan byte pasti乱码, jadi modifikasi source code yang akan diuji menjadi sebagai berikut, mengkonversi string menjadi []rune, dengan demikian dapat menghindari masalah di atas.

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)
}

Selanjutnya langsung jalankan test case yang gagal dari fuzz testing sebelumnya

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

Dapat dilihat kali ini lulus testing, jalankan lagi fuzz testing lihat masih ada masalah atau tidak

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

Dapat dilihat error lagi, kali ini masalah adalah setelah reverse string dua kali tidak sama, karakter asli adalah \xe4, hasil yang diharapkan adalah 4ex\, tetapi hasilnya乱码, sebagai berikut

go
func main() {
  fmt.Println("\xe4")
  fmt.Println([]byte("\xe4"))
  fmt.Println([]rune("\xe4"))
  fmt.Printf("%q\n", "\xe4")
  fmt.Printf("%x\n", "\xe4")
}

Hasil eksekusinya adalah


[65533]
"\xe4"
e4

Menelusuri penyebabnya adalah \xe4 mewakili satu byte, tetapi bukan urutan UTF-8 yang valid (dalam encoding UTF-8 \xe4 adalah awal karakter tiga byte, tetapi缺少 dua byte berikutnya). Saat dikonversi ke []rune, Golang otomatis mengubahnya menjadi []rune yang berisi单个 karakter Unicode []rune{"\uFFFD"}, setelah reverse masih []rune{"\uFFFD"}, saat dikonversi kembali ke string karakter Unicode ini diganti dengan encoding UTF-8-nya \xef\xbf\xbd. Oleh karena itu satu solusi adalah jika yang传入 adalah string non-utf8, langsung return error:

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
}

Kode testing juga perlu dimodifikasi sedikit

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)
    }
  })
}

Saat fungsi reverse mengembalikan error, skip testing, jalankan fuzz testing lagi

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

Lalu kali ini dapat mendapatkan log output fuzz testing yang lebih lengkap, di antaranya penjelasan beberapa konsep sebagai berikut:

  • elapsed: waktu yang telah berlalu setelah satu putaran selesai
  • execs: total input yang dijalankan, 297796/sec表示 berapa banyak input per detik
  • new interesting: di testing, total input "interesting" yang telah ditambahkan ke corpus. (Input interesting mengacu pada input yang dapat memperluas coverage kode di luar jangkauan corpus yang ada, seiring dengan perluasan coverage, tren pertumbuhannya secara umum akan terus melambat)

TIP

Jika tidak ada parameter -fuzztime untuk membatasi waktu, fuzz testing akan berjalan selamanya.

Dukungan Tipe

Tipe yang didukung di Go Fuzz sebagai berikut:

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

Golang by www.golangdev.cn edit