36 / 73

Testlash asoslari

Ko'plab dasturlash tillarida test yozish qo'shimcha freymvorklar o'rnatishni talab qiladi: test runner tanlaysiz, assertion kutubxonalarini o'rnatasiz va har xil mock vositalari bo'yicha bahslashasiz. Go bularning barchasini standart vositalar zanjirining (toolchain) o'ziga kiritish orqali hal qilgan. testing paketi standart kutubxona bilan birga keladi, go test buyrug'i kompilyatorning o'ziga o'rnatilgan, test fayllarini nomlash va joylashtirish qoidalari esa hamma uchun qat'iy va yagona.

Murakkab assertion sintaksislari (DSL) yoki expect(x).to.equal(y) kabi sun'iy zanjirlar yo'q. Goda test — bu *testing.T argumentini qabul qiladigan va kutilgan natija chiqmaganda uning metodlarini chaqiruvchi oddiy Go funksiyasidir.

Testning tuzilishi

Test kodi _test.go bilan tugaydigan faylda, tekshirilayotgan asosiy kodning yonida joylashadi. Oddiy go build bu fayllarni butunlay e'tiborsiz qoldiradi — ular faqat go test ishga tushirilgandagina kompilyatsiya qilinadi:

example.go
// math.go
package mathutil

func Add(a, b int) int {
    return a + b
}
example.go
// math_test.go
package mathutil

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Add(2, 3) = %d; want 5", result)
    }
}

Uni go test bilan ishga tushiring, yoki faqat o'tdi/o'tmadi xulosasi o'rniga har bir test nomini ishlash paytida ko'rish uchun go test -v bilan. go test funksiyani umuman topishi uchun uchta qoida amal qiladi: u _test.go faylida bo'lishi, nomi Test bilan boshlanishi va yagona *testing.T parametrini olib, hech narsa qaytarmasligi kerak. Ulardan birortasini buzsangiz — masalan, Testadddagi kichik t — go test funksiyani hech qanday ogohlantirishsiz jimgina o'tkazib yuboradi. Bu "nega testim ishlamayapti?" chalkashligining keng tarqalgan manbai.

Funksiya ichida *testing.T muvaffaqiyatsizlikni xabar qilish uchun kichik, ataylab jozibasiz API beradi:

example.go
if got != want {
    t.Errorf("got %v, want %v", got, want)   // muvaffaqiyatsizlikni qayd etadi, ishlashda davom etadi
}

if err != nil {
    t.Fatalf("unexpected error: %v", err)    // muvaffaqiyatsizlikni qayd etadi, bu testni darhol to'xtatadi
}

Error/Errorf va Fatal/Fatalf orasidagi farq ko'rinadiganidan muhimroq. Errorf testni muvaffaqiyatsiz deb belgilaydi, lekin funksiyani davom ettirishga yo'l qo'yadi — bu bitta ishga tushirishda birinchi xatoda to'xtash o'rniga bir nechta mustaqil muammoni xabar qilmoqchi bo'lganingizda foydali. Fatalf esa testni muvaffaqiyatsiz deb belgilaydi va runtime.Goexitni chaqirib, joriy goroutine'ni darhol yechadi. Bu oldindan shart bajarilmaganda va davom etish shunchaki chalkash, ketma-ket xatolar yoki nil-pointer panic keltirib chiqaradigan paytda to'g'ri tanlov. Keng tarqalgan xato — t.Fatalni testning o'z goroutine'idan boshqasidan chaqirish. U shu goroutine'ning stack'ini yechish orqali ishlagani uchun, uni siz ishga tushirgan yordamchi goroutine'dan chaqirish testni emas, o'sha goroutine'ni tugatadi, va test qotib qoladi yoki kutilmagan tarzda tugaydi. Tuzatish oddiy: xatoni channel orqali qaytarib yuboring va t.Fatalni asosiy test goroutine'ida chaqiring.

Standart kutubxonada t.AssertEqual yoki shunga o'xshash narsa yo'q, va bu unutish emas, dizayn tanlovi. Go mualliflari yillar davomida (asosan muvaffaqiyatli) shunday asoslab kelishgan: assertion kutubxonalari noaniq muvaffaqiyatsizlik xabarlarini ("expected true, got false") rag'batlantiradi va taqqoslash mantig'ini noto'g'ri ketganda o'zi debug qilishni talab qiladigan abstraksiya ortiga yashiradi. ifni o'zingiz yozish uch qatorga tushadi va muvaffaqiyatsizlik xabari nima deyishi ustidan to'liq nazorat beradi — tunda soat 2 da CI chiqishiga tikilib turganingizda bu tejalgan uch qatordan muhimroq.

ifni o'zingiz yozish uch qatorga tushadi va muvaffaqiyatsizlik xabari nima deyishi ustidan to'liq nazorat beradi.

Table-driven testlar

Go testlashidagi eng o'ziga xos kelishuv — table-driven test: har bir holat uchun alohida test funksiyasi o'rniga, holatlar slice'i ustida aylanadigan bitta test funksiyasi yoziladi. U deyarli spetsifikatsiya kabi o'qiladi.

example.go
func TestAdd(t *testing.T) {
    tests := []struct {
        name string
        a, b int
        want int
    }{
        {"positive numbers", 2, 3, 5},
        {"negative numbers", -2, -3, -5},
        {"zero", 0, 5, 5},
        {"mixed signs", -2, 3, 1},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Add(tt.a, tt.b)
            if got != tt.want {
                t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want)
            }
        })
    }
}

Bu andoza ixchamlikdan tashqari sabablar bilan ham o'zini oqlaydi. Holat qo'shish yangi funksiya emas, bir qatorli diff, bu esa hamkasbingiz — yoki kelajakdagi o'zingiz — hozirgina o'ylab topgan chekka holatni aslida yozib qo'yish to'sig'ini pasaytiradi. Test ma'lumotlari bir qarashda ko'z yugurtirib chiqish mumkin bo'lgan blokda turadi, shuning uchun ko'rib chiquvchi yetishmayotgan holatlarni (MinInt overflow haqida-chi?) payqay oladi. Va har bir holat bir xil bajarish va assertion mantig'ini ulashgani uchun, bu mantiq o'nta nusxa ko'chirilgan funksiyada nozik farq qilib ketish o'rniga bir marta ishga tushiriladi va debug qilinadi — tartibsiz yozilgan Go test fayllari odatda shunday bo'lib qoladi.

Sikl ichidagi t.Run chaqiruvi natijani chiroyli guruhlashdan ko'proq ish qiladi. U subtest yaratadi, va subtest'larni tushunish idiomatik Go testlashning ikkinchi yarmi.

Subtest'lar

t.Run(name, func(t *testing.T) { ... }) o'z funksiyasini mustaqil xabar qilinadigan test sifatida, ota-test ostiga joylashtirib ishga tushiradi. Har bir subtest o'zining o'tdi/o'tmadi holatiga, o'zining t.Fatal yechilishiga (bitta subtest'dagi muvaffaqiyatsizlik hali ishga tushmagan qo'shnilarini bekor qilmaydi) va — vositalar uchun hal qiluvchi — murojaat qilish mumkin bo'lgan nomga ega. Bu nom ota-test nomini va subtest nomini slash bilan birlashtirib quriladi: TestAdd/positive_numbers. Shu maqsadda nomdagi bo'shliqlar pastki chiziqlarga almashtiriladi.

Aynan shu murojaat qilinadigan nom -run flagini "shu bitta test funksiyasini ishga tushir"dan ko'ra foydaliroq qiladi:

bash
go test -run TestAdd                  # TestAdd ostidagi har bir subtest
go test -run TestAdd/positive_numbers # aynan bitta holat
go test -run TestAdd/negative         # regexp bo'yicha mos keladi, shuning uchun "negative.*" ham ishlaydi

-run to'liq slash bilan birlashtirilgan nomga moslashtiriladigan regular expression qabul qiladi, ya'ni test fayliga tegmasdan ishga tushirishni ixtiyoriy aniqlikda cheklashingiz mumkin. Table-driven testda qirq holat bor va ulardan biri beqaror bo'lsa, boshqa o'ttiz to'qqiztasini har safar kutib o'tirmasdan faqat o'shanisini qayta-qayta ishga tushira olasiz.

Subtest'lar faqat table-driven sikllar uchun emas. Haqiqatan bog'liq, lekin parametrik jihatdan bir xil bo'lmagan tekshiruvlarni bitta ota-test ostida guruhlash ham qonuniy foydalanish:

example.go
func TestCalculator(t *testing.T) {
    t.Run("addition", func(t *testing.T) {
        if got := Add(2, 3); got != 5 {
            t.Errorf("Add(2, 3) = %d; want 5", got)
        }
    })
    t.Run("subtraction", func(t *testing.T) {
        if got := Subtract(5, 3); got != 2 {
            t.Errorf("Subtract(5, 3) = %d; want 2", got)
        }
    })
}

Bitta tafsilot deyarli hammani kamida bir marta adashtiradi: sikl o'zgaruvchisini ushlab olish. Go 1.22 dan beri for _, tt := range tests har iteratsiyada yangi tt yaratadi, shuning uchun t.Runga uzatilgan closure o'z iteratsiyasidagi qiymatni to'g'ri ushlaydi. Go 1.21 va undan oldingi versiyalarda tt iteratsiyalar bo'ylab qayta ishlatiladigan yagona o'zgaruvchi edi, va unga to'g'ridan-to'g'ri murojaat qilgan closure — ayniqsa keyingi bobda ko'radigan t.Parallel() subtest'lari ichida — goroutine haqiqatan ishga tushgan paytda tt nimani ushlab tursa, shuni ko'rardi, odatda slice'dagi oxirgi elementni. Tarixiy tuzatish sikl tanasining tepasida o'zgaruvchini shadow qilish edi (tt := tt). Bu qatorni hamon ko'plab eski kodda va 1.22 dan oldingi toolchain'larni qo'llab-quvvatlash uchun yozilgan kodda uchratasiz; uni saqlash zararsiz, faqat zamonaviy Go versiyasida endi majburiy emas.

Bilish foydali bo'lgan ikkita kichik sozlash yordamchisi

Go 1.24 da qo'shilgan ikkita testing.T metodi avval qo'lda bir oz ishlashni talab qilgan sozlash andozalarini qoplaydi. t.Context() test tugaganda avtomatik bekor qilinadigan context.Context qaytaradi (aniqrog'i, har qanday t.Cleanup funksiyalari ishga tushishidan oldin). Sinalayotgan kodga haqiqiy context berish uchun bu foydali: context.WithCancelni qo'lda yozib, cancel funksiyasini o'zingiz chaqirishni eslab yurish shart emas:

example.go
func TestFetch(t *testing.T) {
    data, err := Fetch(t.Context(), "https://example.com")
    if err != nil {
        t.Fatalf("Fetch: %v", err)
    }
    // ...
}

t.Chdir(dir) test davomida jarayonning ishchi katalogini o'zgartiradi va test muvaffaqiyatsiz bo'lsa yoki panic qilsa ham, t.Cleanup orqali uni keyin avtomatik tiklaydi. U avval asl katalogni os.Getwd() bilan olib, unga qaytish uchun o'zingizning os.Chdir cleanup'ingizni ro'yxatdan o'tkazishni talab qilgan andozani almashtiradi:

example.go
func TestLoadFromCWD(t *testing.T) {
    t.Chdir(t.TempDir()) // sinalayotgan nisbiy yo'l kodi endi bo'sh vaqtinchalik katalogni ko'radi
    // ...
}

Ikkalasi ham to'g'rilik uchun emas, test to'plami bo'ylab bir xil olti qatorli sozlash/yechish boilerplate'ini takrorlamaslik uchun muhim. Ularning mavjudligini bilganingizdan keyin, t.TempDir() va t.Cleanup() kabi, ularga odatiy tarzda murojaat qilish foydali.

Yaxshi muvaffaqiyatsizlik xabari nima deydi

Xabar formatiga to'xtalib o'tish foydali, chunki bu oylar o'tib, CI'da, siz debugger oldida bo'lmagan paytda test haqiqatan muvaffaqiyatsiz bo'lganda har safar o'zini oqlaydigan odat. t.Errorf("Add(%d, %d) = %d; want %d", a, b, got, want) kelishuvi o'zboshimchalik emas: u muvaffaqiyatsiz bo'lgan aniq chaqiruvni kodda o'qiladigan tartibda, keyin aslida nima bo'lganini va nima bo'lishi kerakligini qayta hosil qiladi. Buni t.Error("test failed") bilan solishtiring — u kelajakdagi o'quvchiga testni yozayotganda siz allaqachon bilgan narsani bilish uchun endi debugger ochishi kerakligidan boshqa hech narsa aytmaydi. Xabarga sarflangan bir necha soniya testni tasodifan buzadigan keyingi odamga o'n daqiqa tejaydi.

Asosiy lug'at joyida — test funksiyalari, t.Errorf va t.Fatalf, table-driven holatlar va nomi bilan murojaat qilinadigan subtest'lar — endi Go test to'plamining katta qismini yozish uchun kerak bo'lgan hamma narsa sizda bor. Hali yetishmayotgani: kod bazasi bir necha fayldan oshib ketganda bu testlar qayerda yashashi, katta yoki binar test kirishlarini test kodidan tashqarida qanday saqlash va testlaringiz o'zingiz o'ylagan kodni aslida ishga tushirayotganini qanday bilish. Aynan shu yerda tashkil qilish va coverage vositalari kiradi, bu keyingi bobning mavzusi.