Skip to content

Test

Geliştiriciler için, iyi testler programdaki hataları erken tespit edebilir. Bakımın zamanında yapılmaması nedeniyle Bug oluşmasından kaynaklanan zihinsel yükü önler. Bu nedenle test yazmak çok gereklidir. Go test konusunda çok basit ve kullanışlı komut satırı aracı go test sağlar. Standart kütüphane ve birçok açık kaynak framework'te testin izlerini görebiliriz. Bu araç kullanımı çok rahattır. Şu anda aşağıdaki test türlerini destekler:

  • Örnek test
  • Birim test
  • Kıyaslama testi
  • Bulanık test

Go'da API'lerin çoğu standart kütüphane testing tarafından sağlanır.

::: ipucu

Komut satırında go help testfunc komutunu çalıştırarak, Go'nun yukarıdaki dört test türü için resmi açıklamasını görebilirsiniz.

:::

Yazım Kuralları

Test yazmaya başlamadan önce, birkaç kurala dikkat etmek gerekir. Bu şekilde后续 öğrenimde daha rahat olacaktır.

  • Test paketi: Test dosyaları en iyi ayrı bir pakette tutulur. Bu paket genellikle test olarak adlandırılır.
  • Test dosyası: Test dosyaları genellikle _test.go ile biter. Örneğin belirli bir fonksiyonu test etmek istiyorsanız, function_test.go olarak adlandırın. Test türüne göre daha da detaylı bölmek istiyorsanız, test türünü dosya öneki olarak kullanabilirsiniz. Örneğin benchmark_marshaling_test.go veya example_marshaling_test.go.
  • Test fonksiyonu: Her test dosyasında farklı testler için birkaç test fonksiyonu bulunur. Farklı test türleri için, test fonksiyonlarının adlandırma stili de farklıdır. Örneğin örnek test ExampleXXXX, birim test TestXXXX, kıyaslama testi BenchmarkXXXX, bulanık test FuzzXXXX şeklindedir. Bu şekilde yorum gerekmeden ne tür bir test olduğu bilinir.

::: ipucu

Paket adı testdata olduğunda, bu paket genellikle test için yardımcı verileri depolamak içindir. Test çalıştırılırken, Go testdata adlı paketi yoksayar.

:::

Yukarıdaki kurallara uyarak iyi bir test stili geliştirmek, günlük bakım için birçok sıkıntıyı önler.

Test Çalıştırma

Test çalıştırmak için esas olarak go test komutu kullanılır. Aşağıda gerçek kod örneği ile açıklanacaktır. Şimdi test edilecek dosya /say/hello.go var, kod şu şekildedir

go
package say

import "fmt"

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

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

Ve test dosyası /test/example_test.go kodu şu şekildedir

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
}

Bu testleri çalıştırmanın birkaç yolu vardır. Örneğin test paketi altındaki tüm test durumlarını çalıştırmak istiyorsanız, doğrudan test dizininde aşağıdaki komutu çalıştırabilirsiniz

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

./ mevcut dizini temsil eder. Go, test dizinindeki tüm test dosyalarını yeniden derledikten sonra, tüm test durumlarını çalıştırır. Sonuçtan tüm test durumlarının geçtiği görülebilir. Sonraki parametre birden fazla dizin takip edebilir. Aşağıdaki komut gibi. Projenin ana dizininde açıkça çalıştırılacak test dosyası yok.

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

::: ipucu

Çalıştırma parametresi birden fazla paket olduğunda, Go başarıyla geçen test durumlarını tekrar çalıştırmaz. Çalıştırma sırasında satır sonuna (cached) ekler. Bu şekilde çıktının bir önceki önbellek olduğunu belirtir. Test bayrak parametresi aşağıdaki kümede olduğunda, Go test sonuçlarını önbelleğe alır. Aksi takdirde önbelleğe almaz.

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

Önbelleği devre dışı bırakmak istiyorsanız, -count=1 parametresini ekleyebilirsiniz.

:::

Elbette belirli bir test dosyasını tek başına çalıştırmak için de belirleyebilirsiniz.

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

Veya belirli bir test dosyasının belirli bir test durumunu tek başına belirleyebilirsiniz. Örneğin

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

Yukarıdaki üç durumun hepsi testi tamamladı. Ancak çıktı sonucu çok öz. Bu durumda -v parametresini ekleyerek çıktı sonucunu daha detaylı hale getirebilirsiniz. Örneğin

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

Artık her test durumunun çalıştırma sırasını, süresini, çalıştırma durumunu ve toplam süreyi açıkça görebilirsiniz.

::: ipucu

go test komutu varsayılan olarak tüm birim testleri, örnek testleri, bulanık testleri çalıştırır. -bench parametresi eklenirse, tüm türlerdeki testler çalıştırılır. Aşağıdaki komut gibi

sh
$ go test -bench .

Bu nedenle -run parametresini belirtmek gerekir. Örneğin sadece tüm kıyaslama testlerini çalıştırmak için komut aşağıdaki gibidir

sh
$ go test -bench . -run ^$

:::

Yaygın Parametreler

Go testinin çok sayıda bayrak parametresi vardır. Aşağıda sadece yaygın parametreler tanıtılacaktır. Daha fazla detay öğrenmek istiyorsanız, go help testflag komutunu kullanarak kendiniz öğrenebilirsiniz.

ParametreAçıklama
-o fileDerlemeden sonra ikili dosya adını belirt
-cSadece test dosyasını derle, ancak çalıştırma
-jsonTest günlüklerini json formatında çıktı
-exec xprogTesti xprog ile çalıştır, go run ile eşdeğerdir
-bench regexpregexp ile eşleşen kıyaslama testlerini seç
-fuzz regexpregexp ile eşleşen bulanık testleri seç
-fuzztime tBulanık testin otomatik olarak sonlanma süresi, t zaman aralığıdır. Birim x olduğunda, sayıyı temsil eder. Örneğin 200x
-fuzzminimizetime tMod testi çalıştırma minimum süresi, kural yukarıdaki gibi
-count nTesti n kez çalıştır, varsayılan 1 kez
-coverTest kapsam analizini aç
-covermode set,count,atomicKapsam analizi modunu ayarla
-cpuTest çalıştırma için GOMAXPROCS
-failfastİlk test başarısız olduktan sonra, yeni test başlatmaz
-list regexpregexp ile eşleşen test durumlarını listele
-parallel nt.Parallel çağıran test durumlarının paralel çalışmasına izin ver. n değeri paralel maksimum sayıdır
-run regexpSadece regexp ile eşleşen test durumlarını çalıştır
-skip regexpregexp ile eşleşen test durumlarını atla
-timeout dTek test çalıştırma süresi d zaman aralığını aşarsa, panic oluşur. d zaman aralığıdır. Örn 1s,1ms,1ns vb.
-shuffle off,on,NTest çalıştırma sırasını karıştır. N rastgele tohumdur. Varsayılan tohum sistem zamanıdır
-vDaha detaylı test günlüğü çıktısı
-benchmemKıyaslama testinin bellek tahsisini istatistik
-blockprofile block.outTestte goroutine engelleme durumunu istatistik ve dosyaya yaz
-blockprofilerate nGoroutine engelleme istatistik sıklığını kontrol et. go doc runtime.SetBlockProfileRate komutu ile daha fazla detay görün
-coverprofile cover.outKapsam testi durumunu istatistik ve dosyaya yaz
-cpuprofile cpu.outCPU durumunu istatistik ve dosyaya yaz
-memprofile mem.outBellek tahsis durumunu istatistik ve dosyaya yaz
-memprofilerate nBellek tahsis istatistik sıklığını kontrol et. go doc runtime.MemProfileRate komutu ile daha fazla detay görün
-mutexprofile mutex.outKilit rekabet durumunu istatistik ve dosyaya yaz
-mutexprofilefraction nn goroutine'in bir karşılıklı kilit için rekabet durumunu istatistik ayarla
-trace trace.outÇalıştırma takip durumunu dosyaya yaz
-outputdir directoryYukarıdaki istatistik dosyalarının çıktı dizinini belirt. Varsayılan go test çalıştırma dizinidir

Örnek Test

Örnek test, diğer üç test gibi programın sorunlarını bulmak için değildir. Daha çok belirli bir fonksiyonun kullanım yöntemini göstermek içindir. Dokümantasyon rolü oynar. Örnek test resmi bir tanımlanmış kavram değildir. Sert bir规范 de değildir. Daha çok mühendislikte geleneksel bir anlaşma gibidir. Uyulup uyulmaması geliştiriciye bağlıdır. Örnek test standart kütüphanede çok sık görülür. Genellikle resmi olarak yazılan standart kütüphane kod örnekleridir. Örneğin standart kütüphane context/example_test.go içindeki ExampleWithDeadline test fonksiyonu. Bu fonksiyon DeadlineContext'in temel kullanım yöntemini gösterir:

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
}

Yüzeyde bu test fonksiyonu sıradan bir fonksiyon gibidir. Ancak örnek test esas olarak Output yorumu ile yansıtılır. Test edilecek fonksiyon sadece bir satır çıktıya sahip olduğunda, Output yorumu kullanılarak çıktı tespit edilir. Önce hello.go adında bir dosya oluşturun, aşağıdaki kodu yazın

go
package say

import "fmt"

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

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

SayHello fonksiyonu test edilecek fonksiyondur. Ardından test dosyası example_test.go oluşturun, aşağıdaki kodu yazın

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
}

Fonksiyonda Output yorumu, fonksiyon çıktısının hello olup olmadığını tespit ettiğini belirtir. Ardından test komutunu çalıştırarak sonuca bakalım.

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

Sonuçtan tüm testlerin geçtiği görülebilir. Output ile ilgili birkaç yazım şekli vardır. Birincisi sadece bir satır çıktıdır. Bu fonksiyon çıktısının hello olup olmadığını tespit etmek anlamına gelir

// Output:
// hello

İkincisi çok satırlı çıktıdır. Yani sırayla çıktının eşleşip eşleşmediğini tespit eder

// Output:
// hello
// bye

Üçüncüsü sırasız çıktıdır. Yani sıra olmadan çok satırlı çıktı eşleşir

// Unordered output:
// bye
// hello

Dikkat edilmesi gereken, test fonksiyonu için, sadece son birkaç satır Output yorumu olduğunda örnek test olarak kabul edilir. Aksi takdirde sadece sıradan bir fonksiyondur. Go tarafından çalıştırılmaz.

Birim Test

Birim test, yazılımdaki en küçük test edilebilir birimi test etmektir. Birimin boyut tanımı geliştiriciye bağlıdır. Bir yapı, bir paket, bir fonksiyon veya bir tür olabilir. Aşağıda yine örnek ile gösterilecektir. Önce /tool/math.go dosyası oluşturun, aşağıdaki kodu yazın

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
}

Ardından test dosyası /tool_test/unit_test.go oluşturun. Birim test için, adlandırma unit_test olabilir veya test edilecek paket ya da fonksiyon dosya öneki olarak kullanılabilir.

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

Birim test için, her test durumunun adlandırma stili TestXXXX şeklindedir. Ve fonksiyonun girdisi t *testing.T olmalıdır. testing.T, testing paketi tarafından test için sağlanan yapıdır. Birçok kullanılabilir metod sağlar. Örnekteki t.Errorf, t.Logf ile eşdeğerdir. Test başarısız günlük bilgilerini formatlamak için kullanılır. Diğer yaygın kullanılanlar arasında t.Fail bulunur. Mevcut durumu test başarısız olarak işaretlemek için kullanılır. Benzer işlevlere sahip t.FailNow da test başarısız olarak işaretler. Ancak前者 başarısız olduktan sonra çalışmaya devam eder. İkincisi ise doğrudan çalışmayı durdurur. Aşağıdaki örnek gibi, beklenen sonucu yanlış sonuç olarak değiştirin:

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内部使用的是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内部使用的是t.FailNow()
    t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
  }
  t.Log("test finished")
}

Yukarıdaki testi çalıştırın, çıktı aşağıdaki gibidir

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

Test günlüğünden TestSum durumunun başarısız olmasına rağmen test finished çıktığı görülebilir. TestEqual ise çıkmadı. Aynı şekilde t.SkipNow da bulunur. Mevcut durumu SKIP olarak işaretler. Ardından çalışmayı durdurur. Bir sonraki test turunda çalışmaya devam eder.

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内部使用的是t.Fail()
    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 {
        // Fatalf内部使用的是t.FailNow()
    t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
  }
  t.Log("test finished")
}

Testi çalıştırırken, test sayısını 2 olarak değiştirin

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

Yukarıdaki örnekte son satırda test finished çıktısı vardır. Testin bittiğini belirtmek için kullanılır. Aslında t.Cleanup kullanarak bir sonlandırma fonksiyonu kaydedebilirsiniz. Bu fonksiyon test durumu bittiğinde çalıştırılır. Aşağıdaki gibi.

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

Testi çalıştırdıktan sonra çıktı aşağıdaki gibidir

$ 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

t.Helper() aracılığıyla mevcut fonksiyon yardım fonksiyonu olarak işaretlenebilir. Yardım fonksiyonu çalıştırılacak ayrı bir test durumu olarak kullanılmaz. Günlük kaydederken çıktı satır numarası da yardım fonksiyonunu çağıranın satır numarasıdır. Bu şekilde günlük analizinde konumlandırma daha doğru olur. Gereksiz diğer bilgilerden kaçınır. Örneğin yukarıdaki t.Cleanup örneği yardım fonksiyonu olarak değiştirilebilir. Aşağıdaki gibi.

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

Testi çalıştırdıktan sonra çıktı bilgileri aşağıdaki gibidir. Önceki ile farkı, test finished satır numarasının çağıranın satır numarası olmasıdır.

$ 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

::: ipucu

Yukarıdaki işlemler sadece ana testte yapılabilir. Yani doğrudan çalıştırılan test durumu. Alt testte kullanılırsa panic oluşur.

:::

Alt Test

Bazı durumlarda, bir test durumunda başka test durumlarını test etmeniz gerekebilir. Bu iç içe test durumlarına genellikle alt test denir. t.Run() metodu aracılığıyla. Bu metodun imzası aşağıdaki gibidir

go
// Run metodu alt testi çalıştırmak için yeni bir goroutine açar. f fonksiyonu çalıştıktan sonra bloke olur ve bekler
// Dönüş değeri testin geçip geçmediğidir
func (t *T) Run(name string, f func(t *T)) bool

Aşağıda bir örnek bulunmaktadır

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

Çalıştırdıktan sonra sonuç aşağıdaki gibidir

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

Çıktıdan açıkça üst-alt hiyerarşik yapıyı görebilirsiniz. Yukarıdaki örnekte ilk alt test bitmeden ikinci alt test çalıştırılmaz. t.Parallel() kullanarak test durumunu paralel çalıştırılabilir olarak işaretleyebilirsiniz. Bu şekilde çıktı sırası belirsiz olacaktır.

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

Testi çalıştırdıktan sonra çıktı aşağıdaki gibidir

$ 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

Test sonucundan açıkça bir bloke bekleme süreci olduğu görülebilir. Test durumlarını eşzamanlı çalıştırırken, yukarıdaki örnek kesinlikle normal şekilde yapılamaz. Çünkü sonraki kodun senkron çalışması garanti edilemez. Bu durumda bir t.Run() daha iç içe yerleştirebilirsiniz. Aşağıdaki gibi

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

Tekrar çalıştırın, normal çalıştırma sonucunu görebilirsiniz.

$ 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

Tablo Stili

Yukarıdaki birim testte, test girdi verileri manuel olarak bildirilen değişkenlerdir. Veri miktarı küçük olduğunda sorun yoktur. Ancak birden fazla veri grubunu test etmek istediğinizde, artık değişken bildirerek test verisi oluşturmak mümkün değildir. Bu nedenle genel durumda mümkün olduğunca yapı slice formunu kullanın. Yapı geçici olarak bildirilen anonim yapıdır. Çünkü bu kodlama stili tablo gibi göründüğünden, table-driven olarak adlandırılır. Aşağıda bir örnek gösterilecektir. Bu manuel olarak birden fazla değişken bildirerek test verisi oluşturan bir örnektir. Birden fazla veri grubu varsa直觀 değildir. Bu nedenle tablo stiline değiştirin

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

Değiştirildikten sonra kod aşağıdaki gibidir

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

Bu şekilde test verisi daha直觀 görünür.

Kıyaslama Testi

Kıyaslama testi performans testi olarak da adlandırılır. Genellikle programın bellek kullanımı, CPU kullanımı, çalıştırma süresi vb. performans göstergelerini test etmek için kullanılır. Kıyaslama testi için, test dosyası genellikle bench_test.go ile biter. Test durumu fonksiyonu BenchmarkXXXX formatında olmalıdır.

Aşağıda string birleştirme örneğinin performans karşılaştırmasını kıyaslama testi örneği olarak kullanalım. Önce /tool/strConcat.go dosyasını oluşturun. Herkesin bildiği gibi doğrudan string kullanarak + birleştirme performansı çok düşüktür. strings.Builder kullanmak ise çok daha iyidir. /tool/strings.go dosyasında iki fonksiyon oluşturun, iki şekilde string birleştirme yapın.

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

Ardından test dosyası /tool_test/bench_tool_test.go oluşturun, kod aşağıdaki gibidir

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

Test komutunu çalıştırın. Komutta detaylı günlük ve bellek analizi açılmıştır. Kullanılan CPU çekirdek sayısı listesi belirtilmiştir. Ve her test durumu iki tur çalıştırılır. Çıktı aşağıdaki gibidir

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

Aşağıda kıyaslama testinin çıktı sonucunu açıklayalım. goos çalışan işletim sistemini temsil eder. goarh CPU mimarisini temsil eder. pkg testin bulunduğu pakettir. cpu CPU ile ilgili bazı bilgilerdir. Aşağıda her test durumunun sonucu her kıyaslama testinin adı ile ayrılır. İlk sütun BenchmarkConcatDirect-2 içindeki 2 kullanılan CPU çekirdek sayısını temsil eder. İkinci sütundaki 4 koddaki b.N boyutunu temsil eder. Yani kıyaslama testindeki döngü sayısıdır. Üçüncü sütun 277771375 ns/op her döngünün tükettiği süreyi temsil eder. ns nanosaniyedir. Dördüncü sütun 4040056736 B/op her döngünün tahsis ettiği bellek byte boyutunu temsil eder. Beşinci sütun 10000 allocs/op her döngünün bellek tahsis sayısını temsil eder.

Açıkça, test sonucuna göre, strings.Builder kullanmanın performansı + ile string birleştirmekten çok daha yüksektir. Sezgisel veri karşılaştırması ile performansı kıyaslamak kıyaslama testinin amacıdır.

benchstat

benchstat açık kaynaklı bir performans testi analiz aracıdır. Yukarıdaki performans testi örnek sayısı sadece iki gruptur. Örnek sayısı arttığında manuel analiz çok zaman alıcı ve zahmetli olur. Bu araç performans analizi sorunlarını çözmek için doğmuştur.

Önce bu aracı indirmeniz gerekir

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

Kıyaslama testini iki kez çalıştırın. Bu sefer örnek sayısını 5 olarak değiştirin. Ve sırasıyla old.txt ve new.txt dosyalarına çıktı alın. Karşılaştırma için. İlk çalıştırma sonucu

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

İkinci çalıştırma sonucu

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

Ardından benchstat ile karşılaştırın

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

Sonuçtan benchstat'in bunu üç gruba ayırdığı görülebilir. Sırasıyla süre, bellek kullanımı ve bellek tahsis sayısı. geomean ortalama değerdir. p örneğin anlamlılık seviyesidir. Kritik aralık genellikle 0.05'tir. 0.05'ten yüksekse çok güvenilir değildir. Verilerden birini alın:

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

old çalıştırma süresinin 894.7ms, new çalıştırma süresinin 1123.2ms olduğu görülebilir. Buna göre %25.53 süre artışı oldu.

Bulanık Test

Bulanık test GO1.18'de tanıtılan yeni bir özelliktir. Birim test ve kıyaslama testinin bir geliştirmesidir. Farkı, önce ikisinin test verilerinin geliştirici tarafından manuel olarak yazılması gerekirken, bulanık test corpus kullanarak rastgele test verisi üretebilir. Go'daki bulanık test hakkında daha fazla kavram öğrenmek için Go Fuzzing adresine gidebilirsiniz. Bulanık testin avantajı, sabit test verilerine kıyasla, rastgele verinin programın sınır koşullarını daha iyi test edebilmesidir. Aşağıda resmi öğretici örneğini kullanarak açıklayalım. Bu sefer test edilecek bir string ters çevirme fonksiyonu. Önce /tool/strings.go dosyasını oluşturun, aşağıdaki kodu yazın

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

Bulanık test dosyası /tool_test/fuzz_tool_test.go oluşturun, aşağıdaki kodu yazın

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

Bulanık testte, önce corpus tohum kütüphanesine veri eklemeniz gerekir. Örnekte f.Add() kullanarak ekleriz. Bu sonraki rastgele test verisi oluşturmaya yardımcı olur. Ardından f.Fuzz(fn) kullanarak test edin. Fonksiyon imzası aşağıdaki gibidir:

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

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

fn birim test fonksiyon mantığına benzer. Fonksiyonun ilk girdisi t *testing.T olmalıdır. Ardından oluşturmak istediğiniz parametreler gelir. Girilen string öngörülemez olduğundan, iki kez ters çevirme yöntemi kullanarak doğrulama yapıyoruz. Aşağıdaki komutu çalıştırın

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

Parametre -fuzz içermediğinde, rastgele test verisi üretilmez. Sadece corpus'taki veriler test fonksiyonuna girilir. Sonuçtan tüm testlerin geçtiği görülebilir. Bu şekilde kullanmak birim test ile aynıdır. Ancak aslında sorunludur. Aşağıda -fuzz parametresini ekleyerek tekrar çalıştıralım.

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

::: ipucu

Bulanık testte başarısız olan durumlar mevcut test klasörü altındaki testdata dizinindeki belirli bir corpus dosyasına çıktı alınır. Örneğin yukarıdaki örnekte

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

testdata\fuzz\FuzzReverse\d856c981b6266ba2 çıktı corpus dosya yoludur. Dosya içeriği aşağıdaki gibidir

go test fuzz v1
string("𐑄")

:::

Bu sefer geçmediği görülebilir. Nedeni string ters çevrildikten sonra utf8 formatı olmayan hale gelmesidir. Bu nedenle bulanık test ile bu sorun tespit edildi. Bazı karakterlerin kapladığı bir byte'tan fazla olduğundan, byte birimi olarak ters çevrilirse kesinlikle anlamsız olur. Bu nedenle test edilecek kaynak kodu aşağıdaki gibi değiştirin. String'i []rune'a dönüştürün. Bu şekilde yukarıdaki sorunun oluşmasını önleyebilirsiniz.

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

Ardından doğrudan bir önceki bulanık test başarısız durumunu çalıştırın

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

Bu sefer testi geçtiği görülebilir. Tekrar bulanık test çalıştırarak sorun olup olmadığına bakalım

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

Tekrar hata olduğu görülebilir. Bu sefer sorun string'i iki kez ters çevirdikten sonra eşit olmaması. Orijinal karakter \xe4, beklenen sonuç 4ex\. Ancak sonuç anlamsız. Aşağıdaki gibi

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

Çalıştırma sonucu


[65533]
"\xe4"
e4

Nedeni \xe4'ün bir byte'ı temsil etmesi. Ancak geçerli bir UTF-8 dizisi değil (UTF-8 kodlamasında \xe4 üç byte karakterin başlangıcıdır. Ancak sonraki iki byte eksik). []rune'a dönüştürüldüğünde, Golang otomatik olarak tek Unicode karakter []rune{"\uFFFD"} haline getirir. Ters çevrildikten sonra hala []rune{"\uFFFD"}'dir. string'e geri dönüştürüldüğünde bu Unicode karakter tekrar UTF-8 kodlaması \xef\xbf\xbd ile değiştirilir. Bu nedenle bir çözüm girilen utf8 olmayan string ise, doğrudan hata döndürmektir:

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
}

Test kodu da biraz değiştirilmelidir

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

Ters çevirme fonksiyonu error döndürdüğünde, testi atla. Tekrar bulanık test çalıştır

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

Ardından bu sefer nispeten tam bir bulanık test çıktı günlüğü alabilirsiniz. Bazı kavramların açıklamaları aşağıdaki gibidir:

  • elapsed: Bir tur tamamlandıktan sonra geçen süre
  • execs: Çalıştırılan girdi toplam sayısı. 297796/sec saniyede kaç girdi olduğunu gösterir
  • new interesting: Test sırasında, corpus'a eklenen "ilginç" girdilerin toplam sayısı. (İlginç girdi, kod kapsamını mevcut corpus'un kapsayabileceği aralığın ötesine genişletebilen girdiyi ifade eder. Kapsam alanı sürekli genişledikçe, büyüme eğilimi genel olarak yavaşlar)

::: ipucu

-fuzztime parametresi ile süre sınırlanmazsa, bulanık test sonsuza kadar çalışmaya devam eder.

:::

Tür Desteği

Go Fuzz'da desteklenen türler aşağıdaki gibidir:

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

Golang by www.golangdev.cn edit