Go’da test yozish
Test — kodning kutilgan natijani berishini avtomatik tekshiradigan kod.
Masalan, Add(2, 3) funksiyasi 5 qaytarishi kerak bo‘lsa, buni har safar qo‘lda tekshirish shart emas. Test yozib qo‘ysak, Go bu tekshiruvni avtomatik bajaradi.
Go’da buning uchun alohida test framework o‘rnatish shart emas. Standart kutubxonadagi testing paketi va go test buyrug‘i asosiy testlarni yozish uchun yetarli.
Testlar faqat funksiyaning to‘g‘ri natija qaytarishini tekshirish uchun emas. Ular xato holatlari, chegara qiymatlari, fayl bilan ishlash, error qiymatlari va boshqa xatti-harakatlarni ham tekshirishi mumkin.
Birinchi test
Avval juda oddiy funksiyadan boshlaymiz.
Quyidagi hisob paketi ikkita butun sonni qo‘shadi:
Add() ikkita int qiymat qabul qiladi va ularning yig‘indisini qaytaradi.
Masalan:
Endi shu xatti-harakatni test bilan tekshiramiz.
Go test fayllarining nomi _test.go bilan tugashi kerak. Masalan:
Test funksiyasi odatda TestXxx(t *testing.T) ko‘rinishida yoziladi:
package hisob
import "testing"
func TestAdd(t *testing.T) {
got := Add(2, 3)
want := 5
if got != want {
t.Errorf("Add(2, 3) = %d; %d kutilgan", got, want)
}
}
Bu testni bosqichma-bosqich ko‘ramiz.
Avval haqiqiy natija olinadi:
got — funksiya amalda qaytargan qiymat. Bu yerda uning qiymati 5 bo‘lishi kerak.
Keyin kutilgan natijani yozamiz:
want — test bo‘yicha to‘g‘ri deb hisoblangan qiymat.
So‘ng ikkala qiymat solishtiriladi:
Agar got va want teng bo‘lmasa, test muvaffaqiyatsiz hisoblanadi.
Masalan, Add() tasodifan quyidagicha yozilgan bo‘lsa:
Add(2, 3) natijasi -1 bo‘ladi. Test esa 5 kutmoqda. Shunda t.Errorf() xatoni qayd etadi.
Joriy paketdagi testlarni quyidagicha bajarish mumkin:
Modul ichidagi barcha paketlarning testlarini bajarish uchun:
Bu ikki buyruq o‘rtasida muhim farq bor:
faqat joriy paketni test qiladi.
esa joriy modul ichidagi paketlar va ularning quyi paketlarini ham tekshiradi.
t.Errorf() va t.Fatalf() farqi
Testlarda xatoni qayd etishning bir nechta usuli bor. Eng ko‘p uchraydiganlaridan ikkitasi t.Errorf() va t.Fatalf().
t.Errorf() testni muvaffaqiyatsiz deb belgilaydi, lekin joriy test funksiyasi bajarilishini davom ettiradi:
Bu keyingi tekshiruvlarni ham bajarish foydali bo‘lgan holatlarda ishlatiladi.
t.Fatalf() esa xatoni qayd etadi va joriy test funksiyasini shu joyda to‘xtatadi:
Bu odatda keyingi kodni bajarish ma’nosiz bo‘lib qolganida kerak.
Masalan, faylni ochishning o‘zi muvaffaqiyatsiz bo‘lsa, fayl ichidagi ma’lumotni tekshirishning iloji yo‘q. Shunday holatda t.Fatalf() mantiqan to‘g‘riroq.
Muhim jihat shuki, t.Fatalf() butun go test jarayonini to‘xtatmaydi. U aynan joriy testning bajarilishini to‘xtatadi. Boshqa testlar odatda bajarilishda davom etishi mumkin.
Table-driven test va subtest
Ko‘pincha bitta funksiyani faqat bitta qiymat bilan tekshirish yetarli bo‘lmaydi.
Masalan, Add() funksiyasini quyidagi holatlarda tekshirmoqchi bo‘lishimiz mumkin:
- ikkita musbat son;
- ikkita manfiy son;
- son va nol.
Har bir holat uchun alohida Test... funksiyasi yozish mumkin. Lekin Go’da bunday testlarni jadval ko‘rinishida saqlash juda keng tarqalgan.
Bu usul table-driven test deb ataladi.
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b, want int
}{
{name: "musbat", a: 2, b: 3, want: 5},
{name: "manfiy", a: -2, b: -3, want: -5},
{name: "nol", a: 4, b: 0, want: 4},
}
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; %d kutilgan",
tt.a,
tt.b,
got,
tt.want,
)
}
})
}
}
Bu yerda tests — test holatlari saqlanadigan slice.
Har bir element quyidagi ma’lumotlarni saqlaydi:
name test holatining nomi.
a va b — Add() funksiyasiga beriladigan argumentlar.
want — kutilgan natija.
Masalan:
quyidagi tekshiruvni bildiradi:
Keyin barcha holatlar for orqali birma-bir olinadi:
Har bir holat uchun t.Run() chaqiriladi:
t.Run() alohida subtest yaratadi.
Natijada testlar taxminan quyidagi nomlarda ko‘rinadi:
Buning foydasi shundaki, aynan qaysi holat yiqilganini ko‘rish osonlashadi.
Masalan, faqat musbat subtestini ishga tushirish mumkin:
Bitta subtest muvaffaqiyatsiz bo‘lsa, boshqa subtestlar ham odatda bajarilishda davom etadi.
Table-driven test ayniqsa bir xil qoidani ko‘p qiymatlar bilan tekshirish kerak bo‘lganda foydali. Yangi holat qo‘shish uchun yangi test funksiyasi yozish shart emas. Jadvalga yana bitta element qo‘shish kifoya.
Testlarni mustaqil saqlash
Yaxshi test boshqa testning natijasiga yoki bajarilish tartibiga bog‘liq bo‘lmasligi kerak.
Masalan, bunday yondashuv xavfli:
Agar testlarning bajarilish tartibi o‘zgarsa yoki TestB alohida bajarilsa, u yiqilishi mumkin.
Shuning uchun har bir test imkon qadar o‘z holatini o‘zi tayyorlashi va test tugagach tashqi muhitda keraksiz o‘zgarish qoldirmasligi kerak.
Ayniqsa quyidagilarga ehtiyot bo‘lish kerak:
- global holatni o‘zgartirish;
- foydalanuvchining haqiqiy fayllarini ishlatish;
- umumiy database holatiga tayanish;
- tashqi network servislariga keraksiz murojaat qilish;
- boshqa test yaratgan fayl yoki ma’lumotga bog‘lanish.
Fayl bilan ishlaydigan testlarda vaqtinchalik katalog kerak bo‘lishi mumkin. Buning uchun Go t.TempDir() metodini beradi.
func TestSave(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "result.txt")
if err := os.WriteFile(path, []byte("salom"), 0o600); err != nil {
t.Fatalf("fayl yozilmadi: %v", err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("fayl o‘qilmadi: %v", err)
}
if string(data) != "salom" {
t.Errorf("natija = %q; %q kutilgan", data, "salom")
}
}
Bu snippet uchun quyidagi importlar kerak:
Jarayonni bosqichma-bosqich ko‘ramiz.
Avval test uchun vaqtinchalik katalog yaratiladi:
Bu katalog testga tegishli bo‘ladi. Test tugagach Go uni avtomatik tozalaydi.
Keyin shu katalog ichida fayl yo‘li yaratiladi:
filepath.Join() operatsion tizimga mos fayl yo‘lini hosil qiladi.
So‘ng "salom" matni faylga yoziladi:
0o600 Unix uslubidagi permission qiymati:
Agar yozishda xato yuz bersa:
deyiladi.
Bu yerda Fatalf ishlatilishining sababi muhim. Fayl yozilmagan bo‘lsa, keyingi bosqichda uni o‘qish va ichidagi ma’lumotni tekshirishning ma’nosi qolmaydi.
Keyin fayl qayta o‘qiladi:
Va yakunda fayl ichidagi qiymat tekshiriladi:
t.TempDir() yordamida test foydalanuvchining haqiqiy fayllariga bog‘lanmaydi. Bu testlarni xavfsizroq va takror bajarishga qulayroq qiladi.
Coverage
Coverage testlar kodning qaysi qismlarini bajarganini ko‘rsatadi.
Oddiy coverage foizini olish uchun:
Masalan, natijada shunga o‘xshash ma’lumot chiqishi mumkin:
Bu testlar bajarilganda statementlarning qanchasi ishga tushganini bildiradi.
Coverage natijasini faylga saqlash uchun:
Bu buyruq coverage.out nomli profil yaratadi.
Keyin uni HTML ko‘rinishida ochish mumkin:
HTML hisobot qaysi kod testlar orqali bajarilganini va qaysi qism bajarilmaganini ko‘rishni osonlashtiradi.
Lekin coverage foizini kod sifatining to‘liq o‘lchovi deb qabul qilish kerak emas.
Masalan, test funksiya ichidagi barcha satrlarni bajargani bilan noto‘g‘ri natijalarni tekshirmayotgan bo‘lishi mumkin. Bunday test yuqori coverage berishi mumkin, lekin amalda foydasi kam.
Shuning uchun testlarda faqat coverage foiziga emas, tekshirilayotgan xatti-harakatga ham qarash kerak.
Muhim holatlar orasida odatda quyidagilar bor:
- odatiy muvaffaqiyatli yo‘l;
- chegara qiymatlari;
- noto‘g‘ri input;
errorqaytadigan yo‘llar;- nol yoki bo‘sh qiymatlar;
- biznes uchun muhim maxsus holatlar.
Yuqori coverage foydali signal bo‘lishi mumkin. Lekin u kod to‘g‘riligini o‘zi mustaqil ravishda isbotlamaydi.
Misollar
1. Oddiy natijani tekshirish
Bu misol test yozishning eng asosiy shaklini ko‘rsatadi. Funksiya qaytargan haqiqiy qiymat kutilgan qiymat bilan solishtiriladi.
package main
import "testing"
func multiply(a, b int) int {
return a * b
}
func TestMultiply(t *testing.T) {
got := multiply(4, 3)
want := 12
if got != want {
t.Errorf("multiply(4, 3) = %d; %d kutilgan", got, want)
}
}
Testning birinchi muhim qatori:
Bu yerda funksiya amalda nima qaytargani olinadi.
Hisob:
Keyingi qator:
test kutayotgan natijani saqlaydi.
So‘ng:
orqali amaldagi va kutilgan qiymat taqqoslanadi.
Agar multiply(4, 3) 12 qaytarsa, test muvaffaqiyatli tugaydi.
Agar boshqa qiymat qaytarsa:
testni failed holatiga o‘tkazadi.
Bu misoldagi asosiy qoida juda sodda:
Ko‘p Go testlarining asosida aynan shu yondashuv turadi.
2. Xatoni natijadan oldin tekshirish
Funksiya qiymat bilan birga error ham qaytarsa, ko‘pincha errorni birinchi bo‘lib tekshirish kerak.
Quyidagi funksiya ikkita butun sonni bo‘ladi:
package main
import (
"errors"
"testing"
)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("nolga bo‘lish mumkin emas")
}
return a / b, nil
}
func TestDivide(t *testing.T) {
got, err := divide(12, 3)
if err != nil {
t.Fatalf("divide() kutilmagan xato qaytardi: %v", err)
}
want := 4
if got != want {
t.Errorf("divide(12, 3) = %d; %d kutilgan", got, want)
}
}
divide() ikkita natija qaytaradi:
Muvaffaqiyatli holatda:
bo‘ladi.
Masalan:
Shuning uchun:
dan keyin:
bo‘lishi kutiladi.
Birinchi tekshiruv aynan err uchun yozilgan:
Bu yerda t.Fatalf() tanlangan. Sababi divide() xato qaytargan bo‘lsa, got qiymatini normal natija sifatida tekshirish endi mantiqan to‘g‘ri emas.
Faqat xato yo‘q ekaniga ishonch hosil qilgandan keyin:
va:
bajariladi.
Bu misolda 12 va 3 tasodifiy tanlanmagan. 12 / 3 butun son sifatida aniq 4 beradi. Shuning uchun bu test integer division bilan bog‘liq boshqa nozikliklarni aralashtirmaydi.
Asosiy qoida:
3. Chegara qiymatlarini jadvalda tekshirish
Bu misol table-driven test yordamida funksiyaning bir nechta muhim holatini tekshiradi.
package main
import "testing"
func normalize(value int) int {
if value < 0 {
return 0
}
return value
}
func TestNormalize(t *testing.T) {
tests := []struct {
name string
value int
want int
}{
{name: "manfiy", value: -1, want: 0},
{name: "chegara", value: 0, want: 0},
{name: "musbat", value: 5, want: 5},
}
for _, tt := range tests {
got := normalize(tt.value)
if got != tt.want {
t.Errorf(
"%s: normalize(%d) = %d; %d kutilgan",
tt.name,
tt.value,
got,
tt.want,
)
}
}
}
normalize() qoidasi:
Shuning uchun test uchta muhim holatni tekshiradi.
Birinchisi:
-1 < 0 rost. Funksiya 0 qaytarishi kerak.
Ikkinchisi:
Bu aynan shart chegarasi.
Funksiyada:
yozilgan.
0 < 0 yolg‘on. Demak, 0 o‘zgarishsiz qaytariladi.
Uchinchisi:
5 musbat qiymat bo‘lgani uchun funksiya uni o‘zgartirmasdan qaytarishi kerak.
Chegara qiymatlarini test qilish muhim. Chunki <, <=, >, >= kabi operatorlardagi bitta belgining o‘zgarishi funksiyaning aynan chegaradagi xatti-harakatini o‘zgartirib yuborishi mumkin.
Bu misolning asosiy qoidasi — faqat odatiy qiymatlarni emas, shart o‘zgaradigan chegarani ham tekshirish.
4. Holatlarni subtestlarga ajratish
Bu misol table-driven testdagi har bir holatni alohida subtest sifatida bajarishni ko‘rsatadi.
package main
import "testing"
func isEven(value int) bool {
return value%2 == 0
}
func TestIsEven(t *testing.T) {
tests := []struct {
name string
value int
want bool
}{
{name: "juft", value: 8, want: true},
{name: "toq", value: 7, want: false},
{name: "nol", value: 0, want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := isEven(tt.value)
if got != tt.want {
t.Errorf(
"isEven(%d) = %t; %t kutilgan",
tt.value,
got,
tt.want,
)
}
})
}
}
isEven() juftlikni qoldiq orqali tekshiradi:
Masalan:
Shuning uchun 8 juft.
Shuning uchun 7 toq.
Nol uchun:
Demak, 0 ham juft son hisoblanadi.
t.Run() orqali har bir holat o‘z nomiga ega bo‘ladi:
Bu test natijasini o‘qishni osonlashtiradi.
Masalan, faqat juft son holatini bajarish mumkin:
Bu katta test to‘plamida ayniqsa qulay. Muammo faqat bitta holatda bo‘lsa, butun jadvalni qayta-qayta bajarish shart bo‘lmasligi mumkin.
Asosiy qoida — t.Run() test holatlariga alohida nom va alohida subtest kontekstini beradi.
5. Takroriy tekshiruvni helper funksiyaga ajratish
Bir xil test tekshiruvi ko‘p joyda takrorlansa, uni helper funksiyaga chiqarish mumkin.
package main
import "testing"
func add(a, b int) int {
return a + b
}
func assertEqual(t *testing.T, got, want int) {
t.Helper()
if got != want {
t.Errorf("natija = %d; %d kutilgan", got, want)
}
}
func TestAdd(t *testing.T) {
assertEqual(t, add(2, 3), 5)
assertEqual(t, add(-2, 2), 0)
}
Helper funksiya:
bir xil tenglik tekshiruvini bitta joyga to‘playdi.
Muhim qator:
Bu testing paketiga assertEqual() oddiy test logikasi emas, yordamchi test funksiyasi ekanini bildiradi.
Buning foydasi xato hisobotida ko‘rinadi.
Agar helper ichidagi tekshiruv yiqilsa, Go xato joyini imkon qadar helperning ichidagi:
qatoriga emas, helper chaqirilgan test qatoriga bog‘lab ko‘rsatadi.
Masalan:
Bu qaysi tekshiruv muammo berganini tezroq aniqlashga yordam beradi.
Ikkinchi holat:
qarama-qarshi qiymatlarni tekshiradi:
Helper funksiyalar ayniqsa katta testlarda takroriy tekshiruv, setup yoki boshqa yordamchi operatsiyalarni ajratish uchun foydali.
Asosiy qoida — test helper yozsangiz, kerakli joyda t.Helper() orqali uni yordamchi funksiya sifatida belgilash foydali.
6. Test tozalash funksiyasini ro‘yxatdan o‘tkazish
Test davomida vaqtinchalik resource yaratish kerak bo‘lishi mumkin.
Masalan:
- fayl;
- vaqtinchalik server;
- test database holati;
- o‘zgartirilgan global konfiguratsiya.
Bunday resource test tugagach tozalanishi kerak.
t.Cleanup() test yoki subtest tugagandan keyin bajariladigan funksiyani ro‘yxatdan o‘tkazadi.
package main
import "testing"
func TestCleanup(t *testing.T) {
events := make([]string, 0, 2)
t.Run("resource", func(t *testing.T) {
t.Cleanup(func() {
events = append(events, "tozalandi")
})
events = append(events, "ishlatildi")
})
if len(events) != 2 {
t.Fatalf("hodisalar soni = %d; 2 kutilgan", len(events))
}
if events[0] != "ishlatildi" || events[1] != "tozalandi" {
t.Errorf("hodisalar tartibi noto‘g‘ri: %v", events)
}
}
Avval slice yaratiladi:
Bu yerda:
len 0, chunki hali slice ichida hech qanday element yo‘q.
cap 2, chunki bu test ikkita hodisa yozilishini kutmoqda:
Keyin subtest boshlanadi:
Subtest ichida cleanup funksiyasi ro‘yxatdan o‘tkaziladi:
Bu funksiya shu zahoti bajarilmaydi.
Keyingi qator bajariladi:
Shu vaqtda:
Subtest tugayotganida uning cleanup funksiyasi ishlaydi:
t.Run() qaytgan paytda subtestning cleanup funksiyalari bajarib bo‘lingan bo‘ladi. Shuning uchun tashqi test quyidagi tartibni tekshira oladi:
Avval uzunlik tekshiriladi:
Bu yerda Fatalf ishlatilishi mantiqli. Agar slice ichida ikkita element bo‘lmasa, keyingi events[0] yoki events[1] kabi murojaatlar xavfli bo‘lishi mumkin.
Bu misolning asosiy qoidasi — resource yaratgan test uning cleanup ishini ham o‘zi boshqarishi kerak.
7. Vaqtinchalik katalogda fayl tekshirish
Bu misolda test foydalanuvchining haqiqiy kataloglariga tegmasdan fayl yozishni tekshiradi.
package main
import (
"os"
"path/filepath"
"testing"
)
func TestWriteMessage(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "message.txt")
if err := os.WriteFile(path, []byte("salom"), 0o600); err != nil {
t.Fatalf("fayl yozilmadi: %v", err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("fayl o‘qilmadi: %v", err)
}
if string(data) != "salom" {
t.Errorf("fayl = %q; %q kutilgan", data, "salom")
}
}
Birinchi qadam:
Go test uchun vaqtinchalik katalog yaratadi.
Keyingi qadam:
shu katalog ichidagi message.txt fayliga yo‘l hosil qiladi.
So‘ng fayl yoziladi:
[]byte("salom") matnni byte slice ko‘rinishiga aylantiradi.
Permission:
egaga o‘qish va yozish huquqini beradi.
Agar fayl yozilmasa:
testni davom ettirishning ma’nosi yo‘q.
Keyin fayl qayta o‘qiladi:
Agar bu ham muvaffaqiyatli bo‘lsa, o‘qilgan byte slice matnga aylantiriladi:
va kutilgan qiymat bilan solishtiriladi:
Bu test foydalanuvchining masalan:
kabi haqiqiy fayliga tayanmaydi.
t.TempDir() testlarni bir-biridan va tashqi muhitdan yaxshiroq ajratishga yordam beradi. Test tugagach vaqtinchalik katalog ham avtomatik tozalanadi.
8. Sentinel xatoni errors.Is() bilan tekshirish
Go’da ayrim xatolar paket darajasida oldindan yaratiladi:
Bunday xatolar ko‘pincha sentinel error deb ataladi.
Quyidagi misolda funksiya ErrNotFoundni qo‘shimcha kontekst bilan o‘raydi:
package main
import (
"errors"
"fmt"
"testing"
)
var ErrNotFound = errors.New("topilmadi")
func find(id int) error {
if id != 10 {
return fmt.Errorf("%d identifikator: %w", id, ErrNotFound)
}
return nil
}
func TestFindNotFound(t *testing.T) {
err := find(25)
if !errors.Is(err, ErrNotFound) {
t.Fatalf("xato = %v; ErrNotFound kutilgan", err)
}
}
find() qoidasi sodda:
Lekin xato shunchaki:
ko‘rinishida qaytarilmagan.
U %w orqali o‘ralgan:
Masalan, id = 25 bo‘lsa, xato matni taxminan:
ko‘rinishida bo‘ladi.
Lekin xatoning ichki zanjirida ErrNotFound ham saqlanib qoladi.
Shuning uchun testda xato matnini solishtirish o‘rniga:
ishlatiladi.
Bu tekshiruv:
errning o‘zi yoki uning o‘ralgan xatolar zanjiridaErrNotFoundbormi?
degan ma’noni beradi.
Oddiy:
bu misolda ishlamaydi, chunki err tashqi fmt.Errorf() tomonidan yaratilgan boshqa error qiymati.
Xato matnini:
ko‘rinishida tekshirish ham odatda mo‘rt yondashuv. Xabarning matni o‘zgarsa, semantik sabab o‘zgarmagan bo‘lsa ham test yiqiladi.
Bu misolning asosiy qoidasi — o‘ralgan sentinel errorni tekshirish uchun errors.Is()dan foydalanish.
9. Dokumentatsiya misolini test bilan tekshirish
Go’da Example funksiyalar bir vaqtning o‘zida ham dokumentatsiya misoli, ham avtomatik tekshiriladigan test vazifasini bajarishi mumkin.
package main
import "fmt"
func greeting(name string) string {
return "Salom, " + name + "!"
}
func Example_greeting() {
fmt.Println(greeting("Ali"))
// Output: Salom, Ali!
}
greeting() oddiy satr qaytaradi:
Masalan:
natijasi:
bo‘ladi.
Example_greeting() ichida natija stdout’ga chiqariladi:
Keyin maxsus izoh yozilgan:
go test example funksiyani bajarganda haqiqiy stdout natijasini shu Output qiymati bilan solishtiradi.
Bu jarayonni soddalashtirib quyidagicha ko‘rish mumkin:
greeting("Ali")
↓
"Salom, Ali!"
↓
fmt.Println(...)
↓
stdout: Salom, Ali!
↓
// Output: Salom, Ali!
↓
solishtirish
Agar haqiqiy natija va Output mos kelsa, example muvaffaqiyatli o‘tadi.
Masalan, funksiya keyinchalik:
deb o‘zgartirilsa, lekin // Output: yangilanmasa, go test example’ni muvaffaqiyatsiz deb belgilaydi.
Shuning uchun Example faqat o‘quvchi uchun ko‘rsatiladigan kod emas. U dokumentatsiyadagi misolning amaldagi kod bilan mosligini ham avtomatik tekshirishi mumkin.
Bu misoldagi Example_greeting nomi greeting funksiyasiga bog‘langan example’ni bildiradi. go test uni oddiy testlar bilan birga bajarishi mumkin.
Asosiy qoida — // Output: mavjud bo‘lgan Example funksiyalar dokumentatsiya va avtomatik test vazifasini birgalikda bajaradi.