Tarkibga o'tish

Go’da test yozish

Test kodning kutilgan natijani qaytarishini avtomatik tekshiradi. Go’da test yozish uchun standart testing paketi va go test buyrug‘i yetarli.

Birinchi test

Quyidagi paket ikki sonni qo‘shadi:

hisob.go
package hisob

func Add(a, b int) int {
    return a + b
}

Test fayli _test.go bilan tugaydi. Test funksiyasi TestXxx(t *testing.T) ko‘rinishida bo‘ladi:

hisob_test.go
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)
    }
}

Joriy paket testini go test, moduldagi barcha paketlarni esa go test ./... bajaradi:

go test
go test ./...

t.Errorf() xatoni qayd etib, testni davom ettiradi. Keyingi qadamni bajarish ma’nosiz yoki xavfli bo‘lsa, t.Fatalf() xatoni qayd etib, joriy test funksiyasini to‘xtatadi.

Table-driven test va subtest

Bir xil qoidani bir nechta qiymatda tekshirish uchun holatlarni jadvalda saqlash qulay:

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

t.Run() har bir holatga alohida nom beradi. Bitta holat yiqilganda qolganlari bajarilishda davom etadi. Aniq subtestni go test -run 'TestAdd/musbat' bilan ishga tushirish mumkin.

Testlarni mustaqil saqlash

Test natijasi boshqa testning tartibi yoki oldingi holatiga bog‘liq bo‘lmasligi kerak. Global holatni o‘zgartirishdan, real foydalanuvchi fayllaridan va tashqi tarmoqqa keraksiz murojaatdan qoching.

Vaqtinchalik fayl kerak bo‘lsa, t.TempDir() testga tegishli katalog yaratadi va test tugagach uni tozalaydi:

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 importlar os, path/filepath va testing bo‘ladi.

Coverage

Coverage test vaqtida qaysi statementlar bajarilganini ko‘rsatadi:

go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

Yuqori foiz kod to‘g‘riligini isbotlamaydi. Testlar muhim xatti-harakat, chegara qiymatlari va xato yo‘llarini mazmunli tekshirishi kerak.

Xulosa

Testlar _test.go fayllarida TestXxx funksiyalari sifatida yoziladi. go test ./... butun modulni tekshiradi. Jadval testlari ko‘p holatni izchil saqlaydi, t.Run() ularni nomlaydi, t.TempDir() esa faylli testlarni boshqa muhitdan ajratadi.

Misollar

1. Oddiy natijani tekshirish

Bu misolda ko‘paytirish funksiyasining qaytargan qiymati kutilgan natija bilan solishtiriladi.

main_test.go
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)
    }
}

got amaldagi natijani, want esa kutilgan qiymatni saqlaydi. 4 * 3 natijasi 12 bo‘lishi kerak. Tenglik buzilsa, t.Errorf() testni muvaffaqiyatsiz deb belgilaydi.

2. Xatoni natijadan oldin tekshirish

Bu misolda bo‘lish funksiyasi qaytargan error avval tekshiriladi.

main_test.go
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)
    }
}

Xato mavjud bo‘lsa, gotni tekshirish ishonchsiz bo‘lishi mumkin. Shu sabab t.Fatalf() joriy testni darhol to‘xtatadi. 12 / 3 butun son sifatida aniq 4 bo‘lgani uchun shu qiymatlar tanlangan.

3. Chegara qiymatlarini jadvalda tekshirish

Bu misolda bir nechta kirish qiymati bitta table-driven testda saqlanadi.

main_test.go
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)
        }
    }
}

-1 manfiy yo‘lni, 0 shart chegarasini, 5 esa musbat yo‘lni tekshiradi. Funksiyada < 0 ishlatilgani uchun 0 o‘zgarishsiz qoladi. Har bir holat nomi xato xabarini tushunarli qiladi.

4. Holatlarni subtestlarga ajratish

Bu misolda jadvaldagi har bir holat t.Run() orqali alohida subtest sifatida bajariladi.

main_test.go
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)
            }
        })
    }
}

Juft son 2ga bo‘lganda qoldiq 0 bo‘ladi. Shu sabab 0ning o‘zi ham juft hisoblanadi. Subtest nomlari bilan faqat bitta holatni go test -run 'TestIsEven/juft' orqali tanlash mumkin.

5. Takroriy tekshiruvni helper funksiyaga ajratish

Bu misolda bir xil tenglik tekshiruvi assertEqual() helper funksiyasida saqlanadi.

main_test.go
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)
}

t.Helper() Go’ga bu funksiya test yordamchisi ekanini bildiradi. Tekshiruv yiqilsa, xato joyi helper ichidagi satrga emas, assertEqual() chaqirilgan satrga yaqin ko‘rsatiladi. Ikkinchi holatda qarama-qarshi sonlar yig‘indisi 0 bo‘lishi tekshiriladi.

6. Test tozalash funksiyasini ro‘yxatdan o‘tkazish

Bu misolda t.Cleanup() subtest tugagach bajariladigan tozalash amalini belgilaydi.

main_test.go
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)
    }
}

events uzunligi 0, chunki boshida hech bir hodisa sodir bo‘lmagan. Sig‘im 2, chunki ish va tozalashdan iborat ikki yozuv kutiladi. t.Run() qaytganda subtest cleanupi tugagan bo‘ladi, shuning uchun tartibni tashqi testda tekshirish mumkin.

7. Vaqtinchalik katalogda fayl tekshirish

Bu misolda test o‘ziga tegishli katalogda fayl yozib, natijani qayta o‘qiydi.

main_test.go
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")
    }
}

t.TempDir() har bir test uchun alohida katalog yaratadi va test tugagach uni tozalaydi. 0o600 fayl egasiga o‘qish va yozish ruxsatini beradi. Test foydalanuvchining haqiqiy fayllariga bog‘lanmaydi.

8. Sentinel xatoni errors.Is() bilan tekshirish

Bu misolda test o‘ralgan xato ichida kutilgan sabab borligini tekshiradi.

main_test.go
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)
    }
}

Xato %w bilan o‘ralgani uchun uni == yoki matn bilan tekshirish ishonchsiz. errors.Is() zanjirdagi ErrNotFound sababini topadi. 10 mavjud identifikator, 25 esa topilmaydigan qiymat sifatida tanlangan.

9. Generic funksiyani turli turlar bilan tekshirish

Bu misolda bitta generic funksiya int va float64 qiymatlar bilan tekshiriladi.

main_test.go
package main

import "testing"

type Number interface {
    ~int | ~float64
}

func max[T Number](a, b T) T {
    if a > b {
        return a
    }
    return b
}

func TestMax(t *testing.T) {
    t.Run("int", func(t *testing.T) {
        if got := max(4, 9); got != 9 {
            t.Errorf("max(4, 9) = %d; 9 kutilgan", got)
        }
    })

    t.Run("float64", func(t *testing.T) {
        if got := max(7.5, 2.5); got != 7.5 {
            t.Errorf("max(7.5, 2.5) = %g; 7.5 kutilgan", got)
        }
    })
}

Constraint ikkala turda ham > operatoriga ruxsat beradi. Subtestlar tur argumentlari bo‘yicha alohida nomlanadi. Kompilyator argumentlardan Tni aniqlagani uchun [int] va [float64]ni aniq yozish shart emas.

10. Dokumentatsiya misolini test bilan tekshirish

Bu misolda Example funksiyasining stdout natijasi // Output: izohi bilan solishtiriladi.

main_test.go
package main

import "fmt"

func greeting(name string) string {
    return "Salom, " + name + "!"
}

func Example_greeting() {
    fmt.Println(greeting("Ali"))
    // Output: Salom, Ali!
}

Example_greeting paket darajasidagi misol uchun kichik harfli qo‘shimcha nomdan foydalanadi. go test funksiyani bajaradi va chiqarilgan satrni Output qiymati bilan tekshiradi. Natija o‘zgarsa, misol ham test kabi yiqiladi.