Table-driven testlar siz o'ylagan holatlarni tekshirishda a'lo. Ammo ular siz o'ylamagan holatlarni topishga tuzilishiga ko'ra qodir emas. Test jadvalingizdagi har bir kirishni mukammal qayta ishlaydigan parser juftlanmagan surrogate pair'li satr yoki siz sinagandan bir raqam uzunroq son bilan uchrashgan zahoti panic qilishi mumkin. Buning sababi beparvoligingiz emas, balki qo'lda test yozayotgan hech bir inson kirishlar fazosini dushmancha fuzzer yoki production'dagi oddiy omadsizlik oxir-oqibat qiladigandek to'liq qamrab ololmasligidir. Goning fuzzing qo'llab-quvvatlashi aynan shu bo'shliqni yopish uchun mavjud, benchmark'lar esa jadvalli testlar so'ray olmaydigan savolga javob beradi: "bu to'g'rimi" emas, "bu yetarlicha tezmi".
Go 1.18 dan beri toolchain'ga qurilgan fuzz testlash siz yozgan kirishlarga tayanish o'rniga o'zining kirishlarini hosil qiladi. U kod coverage qayta aloqasi bilan boshqariladi, shuning uchun yangi tarmoqlarga yetib boradigan kirishlarni ko'proq mutatsiya qiladi. Siz holatlarni tasvirlamaysiz; har qanday kirish uchun to'g'ri bo'lishi kerak bo'lgan xususiyatlarni (property) tasvirlaysiz.
Fuzz target oddiy test funksiyasiga o'xshaydi, lekin *testing.F qabul qiladi va f.Fuzzni chaqiradi. f.Fuzzga berilgan funksiyaning birinchi parametri *testing.T, undan keyin fuzzer o'zgartirishi kerak bo'lgan qiymatlar keladi:
func FuzzReverse(f *testing.F) {
// seed corpus: boshlash uchun ma'lum qiziqarli kirishlar
f.Add("Hello, world")
f.Add("")
f.Add("a")
f.Fuzz(func(t *testing.T, orig string) {
rev := Reverse(orig)
doubleRev := Reverse(rev)
// xususiyat: ikki marta teskari aylantirish asl satrni qaytaradi
if orig != doubleRev {
t.Errorf("Reverse(Reverse(%q)) = %q, want %q", orig, doubleRev, orig)
}
// xususiyat: teskari aylantirish rune'lar sonini saqlaydi
if utf8.RuneCountInString(orig) != utf8.RuneCountInString(rev) {
t.Errorf("rune count changed: %q -> %q", orig, rev)
}
})
}
Faqat go testni ishga tushirish FuzzReverseni oddiy test sifatida bajaradi: u seed corpus'ga (f.Addga berilgan qiymatlar va testdata/fuzz/FuzzReverse/ ostida allaqachon saqlangan har qanday fayllar) qarshi bir marta ishlaydi va o'tdi yoki o'tmadi deb xabar qiladi. Haqiqiy fuzzing — yangi kirishlarni hosil qilish — faqat uni aniq so'raganingizda sodir bo'ladi:
go test -fuzz=FuzzReverse # cheksiz fuzz qilish, Ctrl-C yoki muvaffaqiyatsizlikkacha
go test -fuzz=FuzzReverse -fuzztime=30s
Fuzzer muvaffaqiyatsiz bo'ladigan kirishni topganda, aynan shu kirishni testdata/fuzz/FuzzReverse/<hash> ga yozadi va uning joylashuvini chop etadi. Shundan boshlab bu fayl doimiy regression testga aylanadi: go test (-fuzz flagi kerak emas) har doim saqlangan corpus yozuvlarining hammasini qayta ijro etadi, shuning uchun fuzzer bir marta topgan xato hech qachon jimgina qaytib kela olmaydi. Buni ataylab versiya nazoratiga commit qilish foydali: qiyinchilik bilan topilgan chekka holatlarga to'la, o'sib boruvchi testdata/fuzz katalogi fuzz bilan sinalgan paket to'plashi mumkin bo'lgan eng qimmatli artefaktlardan biri.
f.Fuzzdagi tip cheklovlari oddiy funksiyanikidan torroq: argumentlar string, []byte yoki asosiy sonli/bool tiplardan biri bo'lishi kerak, chunki fuzzing dvigateli ularni mutatsiya qilib, corpus formatiga serializatsiya qila olishi kerak. Agar haqiqiy funksiyangiz struct yoki maxsus tip qabul qilsa, xom baytlarni fuzz qiling va qiymatni closure ichida quring. Bu shuningdek fuzzer hosil qilgan, sizning sohangizdan tashqaridagi kirishlarni muvaffaqiyatsizlik deb hisoblash o'rniga t.Skip() bilan rad etishning tabiiy joyi:
func FuzzParsePositive(f *testing.F) {
f.Add(42)
f.Fuzz(func(t *testing.T, n int) {
if n <= 0 {
t.Skip() // soha tashqarisida, xato emas
}
result, err := ParsePositive(n)
if err != nil {
t.Fatalf("ParsePositive(%d): unexpected error: %v", n, err)
}
if result != n*2 {
t.Errorf("ParsePositive(%d) = %d, want %d", n, result, n*2)
}
})
}
Fuzzing aynan qo'lda holatlar yozish eng qiyin bo'lgan kodda yaxshi natija beradi: parserlar, dekoderlar, ishonchsiz kirishga tegadigan hamma narsa va aylanib o'tish mumkin bo'lgan teskari amali bor hamma narsa (encode/decode, compress/decompress, marshal/unmarshal). Agar xususiyatni bayon qila olsangiz — "hozirgina kodlaganimni dekodlash asl nusxani qaytaradi", "bu hech qachon panic qilmaydi", "chiqish doim yaroqli UTF-8" — uni buzishi mumkin bo'lgan kirishlarni o'zingiz sanab chiqa olasizmi-yo'qmi, baribir fuzz target yoza olasiz.
Fuzzing "bu hech qachon noto'g'ri javob beradimi" deb so'rasa, benchmark'lar "to'g'ri javob qancha turadi" deb so'raydi. Benchmark funksiyasi Benchmark bilan boshlanadi, *testing.B qabul qiladi va tanasini b.N marta ishga tushiradi. Framework o'lchov barqaror bo'lguncha b.Nni avtomatik oshirib boradi:
func BenchmarkProcessData(b *testing.B) {
data := make([]int, 1000)
for i := range data {
data[i] = i
}
b.ResetTimer() // yuqoridagi sozlashni o'lchovdan chiqarib tashlash
for i := 0; i < b.N; i++ {
processData(data)
}
}
b.ResetTimer() sozlash ishi benchmark funksiyasining o'zi ichida bajarilganda muhim: usiz datani qurish narxi bitta amal vaqtiga taqsimlanadi va processData takroriy chaqiruvlarda aslida qanchalik qimmat ekanini jimgina kam ko'rsatadi. Go 1.24 dan boshlab xuddi shu niyatni ifodalashning toza usuli bor, u qo'lda yozilgan sikl taklif qiladigan nozik tuzoqlardan ham qochadi — tasodifan ish yuki o'rniga ini benchmark qilish yoki b.ResetTimer()ni umuman unutish:
func BenchmarkProcessData(b *testing.B) {
data := make([]int, 1000)
for i := range data {
data[i] = i
}
for b.Loop() {
processData(data)
}
}
b.Loop() tanani xuddi shunday moslashuvchan tarzda aniqlangan marta ishga tushiradi, lekin uning ustidagi sozlashni avtomatik chiqarib tashlaydi va kompilyatorga qo'lda yozilgan for i := 0; i < b.N; i++ sikli bilan ba'zan qila oladigan ishni — sof funksiya chaqiruvini optimallashtirib tashlashni — qilishga yo'l qo'ymaydi. Klassik b.N shaklini hamma joyda, jumladan ko'pchilik mavjud kod bazalarida va hujjatlarning katta qismida ko'rasiz, va u to'liq yaroqli bo'lib qoladi. Lekin zamonaviy toolchain'dagi yangi kodda b.Loop()dan foydalaning.
Benchmark'larni -bench bilan ishga tushiring (u -run test nomlariga mos kelgani kabi regexp'ga moslashtiriladi), va vaqt bilan birga ajratishlar sonini ko'rish uchun -benchmem qo'shing. Bu odatda ancha amaliy raqam, chunki ajratishlar GC bosimini xom nanosekundlar o'zi ko'rsatmaydigan tarzda belgilaydi:
go test -bench=. # paketdagi har bir benchmark
go test -bench=ProcessData
go test -bench=. -benchmem
BenchmarkProcessData-8 1234567 943 ns/op 0 B/op 0 allocs/op
-8 qo'shimchasi ishga tushirish paytidagi GOMAXPROCS, holat raqami emas. Keyingi uch ustun: bajarilgan iteratsiyalar, iteratsiya boshiga nanosekundlar va (-benchmem bilan) iteratsiya boshiga baytlar va ajratishlar. Bir xil funksiyaning ikkita implementatsiyasi bo'lsa va ko'z bilan taxminiy emas, haqiqiy taqqoslash xohlasangiz, ikkalasini ham benchmark sifatida yozing va farq bir necha ishga tushirish bo'yicha statistik ahamiyatlimi-yo'qmi, benchstat (golang.org/x/perf/cmd/benchstat) aytsin. Bitta benchmark chaqiruvi shunchalik shovqinli-ki, bitta ishga tushirishdan chiqqan "12% tezroq" ko'pincha shunchaki shovqin.
t.Run kabi ishlaydigan b.Run bilan sub-benchmark'lar bitta funksiyani turli kirish o'lchamlari bo'ylab aylanib chiqib, har birini alohida nomlangan natija sifatida xabar qilish imkonini beradi:
func BenchmarkProcessData(b *testing.B) {
for _, size := range []int{100, 1_000, 10_000} {
b.Run(fmt.Sprintf("n=%d", size), func(b *testing.B) {
data := make([]int, size)
for b.Loop() {
processData(data)
}
})
}
}
Bu odatda bitta o'zboshimchalik bilan tanlangan o'lchamdagi benchmark'dan ko'proq ma'lumot beradi, chunki u funksiya chiziqli masshtablanishini yoki qandaydir chegaradan keyin keskin yomonlashishini ochib beradi — bu faqat bir hisobotda bir necha o'lchamni yonma-yon ko'rgandagina sezilib qoladigan narsa.
Testning yoki subtest'ning birinchi qatori sifatida chaqirilgan t.Parallel() go testga bu test shu paketdagi boshqa parallel testlar bilan bir vaqtda ishlashi mumkinligini bildiradi. Test funksiyasi darhol qaytadi (uning bajarilishi to'xtatib turiladi, keyin davom ettiriladi), va paketdagi barcha parallel bo'lmagan testlar tugagach, parallel deb belgilangan barcha testlar birga, -parallel belgilagan chegaragacha (standart qiymati GOMAXPROCS) ishlaydi.
func TestSlowOperation(t *testing.T) {
t.Parallel()
// bu boshqa t.Parallel() testlar bilan ustma-ust tushishi mumkin
}
Tezlashish mustaqil, I/O yoki uyquga bog'liq testlardan iborat to'plamlarda haqiqiy, lekin "mustaqil" so'zi bu yerda katta ish qiladi va odamlar aynan shu qismda xato qiladi. Sinxronizatsiyasiz ulashilgan o'zgaruvchan holatga tegadigan parallel testlar shunchaki beqaror chiqish xavfini tug'dirmaydi: go test -race ostida ular butunlay ushlanadi:
// xavfli: ikkala goroutine bir vaqtda bir xil o'zgaruvchini o'zgartiradi
var counter int
func TestA(t *testing.T) {
t.Parallel()
counter++
}
func TestB(t *testing.T) {
t.Parallel()
counter++
}
Tuzatish — bir vaqtda ishlaydigan har qanday kod uchun amal qiladigan intizomning o'zi: har bir testga paket darajasidagi o'zgaruvchilarni ulashish o'rniga o'z holatini bering, va agar fixture haqiqatan ulashilishi kerak bo'lsa (testdata fayli, test konteyneri), har bir parallel test uni faqat o'qishiga ishonch hosil qiling. t.Parallel()ni table-driven subtest'lar bilan birlashtirish keng tarqalgan va samarali andoza, chunki u butun jadvalni mustaqil rejalashtiriladigan ishlar to'plamiga aylantiradi:
func TestValidate(t *testing.T) {
tests := []struct{ name, input string; wantErr bool }{
{"valid email", "a@example.com", false},
{"missing at sign", "example.com", true},
{"empty", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // Go 1.22+ da yaxshi: tt allaqachon har iteratsiyaga xos
_, err := Validate(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("Validate(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
}
})
}
}
Parallel testlarni faqat xatolik sezilgandagina emas, balki doimiy ravishda poyga tekshiruvchisi (race detector) bilan birga ishga tushiring: go test -race ./.... Bu production yuklamasida yuzaga chiqishi mumkin bo'lgan yashirin ma'lumotlar poygasi (data race) xatolarini oldindan aniqlashning eng ishonchli usulidir.
Goroutine'lar va real tizim soati (wall clock) testlarda ko'pincha noqulaylik tug'diradi: time.Sleep chaqirib, "bir necha millisekundda ish bajariladi" deb umid qilish beqaror (flaky) testlarga olib keladi. Bunday testlar sekin ishlaydi va tizim yuklamasiga qarab vaqti-vaqti bilan asossiz yiqiladi.
testing/synctest paketi (Go 1.25 da standart kutubxonaga to'liq kiritilgan) ushbu muammoni testga o'zining mustaqil virtual vaqt hududini berish orqali hal qiladi. synctest.Test(t, f) funksiyasi testni izolyatsiyalangan muhitda ("bubble") ishga tushiradi: bu o'zining virtual soatiga ega bo'lgan goroutine'lar guruhi bo'lib, ushbu virtual vaqt faqat pufakdagi barcha goroutine'lar bloklangan holatda (channel kutish, timer va hokazo) oldinga siljiydi. Ushbu muhitda time.Sleep, context.WithTimeout va time.After haqiqiy soatga emas, virtual soatga qaraydi — natijada 30 soniyalik timeout bir necha millisekund ichida deterministik tarzda sinovdan o'tadi:
func TestPollerTimesOut(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
done := make(chan error, 1)
go func() { done <- poll(ctx) }()
synctest.Wait() // guruhdagi barcha goroutine'lar bloklanishini kutish
// virtual soat t=0 da to'xtab turadi
select {
case err := <-done:
t.Fatalf("poll returned early: %v", err)
default:
}
})
}
synctest.Wait() mexanizmning ikkinchi yarmi: u pufakdagi har bir boshqa goroutine o'zi bloklanmaguncha chaqiruvchi goroutine'ni bloklaydi. Bu testga haqiqiy uyqu bilan taxmin qilish o'rniga aniq belgilangan "hamma narsa joylashdi" nuqtasini kuzatish imkonini beradi. Go 1.27 synctest.Sleepni qo'shdi — time.Sleep va synctest.Waitni "soxta soatni shuncha oldinga surib, keyin hamma narsa qayta joylashishini kut" umumiy holati uchun bitta chaqiruvda birlashtiradigan kichik yordamchi.
Oldindan bilish kerak bo'lgan bitta haqiqiy cheklov bor: pufak goroutine'lari tegadigan va haqiqiy vaqtga sezgir bo'lgan hamma narsa — timer'lar, ticker'lar, deadline'lar — virtuallashtirilishi uchun pufak ichida yaratilishi kerak. Haqiqiy tarmoq chaqiruvi yoki haqiqiy os.Exec soxta soat nima desa ham haqiqiy vaqt oladi, chunki synctest Goning o'z timer mexanizmini virtuallashtiradi, tashqi dunyoni emas. U goroutine'lar, channel'lar va time paketining timer'lari atrofida qurilgan kodni — timeout'li worker pool'lar, backoff bilan qayta urinish mantig'i, kritik yo'lida context.WithTimeout bor hamma narsani — sinash uchun to'g'ri vosita, lekin t.Parallel() yoki I/O uchun mocking framework'ining umumiy o'rnini bosuvchi emas.
Fuzzing to'plam ko'rib chiqadigan kirishlar to'plamini kengaytiradi; benchmark'lar va parallel bajarish esa to'g'rilikka emas, tezlikka murojaat qiladi. Uchtasidan hech biri qolgan ikkitasini almashtirmaydi, va yetuk paket vaqt o'tishi bilan hammasini to'playdi: spetsifikatsiya uchun table-driven testlar, uning markazidagi parser yoki kodek uchun fuzz target'lar va unumdorlik taxmin emas, haqiqiy talab bo'lgan bir necha funksiya uchun benchmark'lar. Keyingi bob bitta paketni izolyatsiyada testlashdan ularning bir nechtasi ulangandan keyin qanday ishlashini testlashga o'tadi: httptest, build tag'lar va unit test bilan to'liq integratsiya testi orasidagi chegara.