Shanraq.org Shanraq.org
Тест: түзеткенде не сынғанын білу
IT

Go тілі: нөлден өз блогыңызға дейін 41-сабақ (барлығы 50)

Тест: түзеткенде не сынғанын білу

Go курсының қырық бірінші сабағы. 10-сабақта уәде етілген кесте: жағдайлар тізімге салынады, t.Run әрқайсысына ат береді, ал құлау қай жағдай сынғанын бірден айтады. Оның үстіне өңдеушіні сервер көтермей тексеру және -cover не көрсететіні туралы адал әңгіме.

Не үшін керек

10-сабақтағы тест бір функцияны бір жағдаймен тексерді. Бұл жағдай бесеу болған күнге дейін жетті.

if-ті бес рет көшіруге болады, солай да істейді — сосын төртінші көшірмеде күтілетін мәнді өзгертуді ұмытады да, тест жылдар бойы басқа нәрсені тексеріп жүреді. Одан жаманы: мұндай тест құлағанда, есепте функцияның аты көрінеді, ал қай жағдай сәйкес келмегені көрінбейді.

Бүгін — Go-да тесттердің барлығы дерлік жазылатын тәсіл. Ол кестелік деп аталады, әрі 10-сабақта мен оны сізде құрылымдар пайда болғанда көрсетуге уәде еткенмін. Олар пайда болды.

Бірден тұтас

Жаңа қалта, go mod init sabaq38. Бірінші файл — main.go, үш таныс нәрсе:

package main

import (
	"fmt"
	"net/http"
	"strings"
)

// parseTags формада тергенді қайталаусыз тізімге айналдырады.
func parseTags(s string) []string {
	var out []string
	seen := map[string]bool{}
	for _, part := range strings.Split(s, ",") {
		t := strings.ToLower(strings.TrimSpace(part))
		if t == "" || seen[t] {
			continue
		}
		seen[t] = true
		out = append(out, t)
	}
	return out
}

// readingTime жоғары қарай дөңгелектейді: оқылмай қалған минут та саналады.
func readingTime(words int) int {
	return (words + 199) / 200
}

func articles() http.Handler {
	titles := map[string]string{"dala": "Дала туралы", "salem": "Сәлем"}

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		slug := strings.TrimPrefix(r.URL.Path, "/read/")
		title, ok := titles[slug]
		if !ok {
			http.Error(w, "мұндай мақала жоқ", http.StatusNotFound)
			return
		}
		fmt.Fprintf(w, "<h1>%s</h1>", title)
	})
}

func main() {
	fmt.Println(parseTags("  Дала , дала,, Блог "))
	fmt.Println(readingTime(400), readingTime(401))
}

Екіншісі — қасындағы main_test.go:

package main

import (
	"net/http"
	"net/http/httptest"
	"slices"
	"strings"
	"testing"
)

func TestParseTags(t *testing.T) {
	cases := []struct {
		name string
		in   string
		want []string
	}{
		{"бос", "", nil},
		{"біреу", "дала", []string{"дала"}},
		{"шеттегі бос орындар", "  дала  ", []string{"дала"}},
		{"әртүрлі регистр", "Дала, дала", []string{"дала"}},
		{"үтірлер арасы бос", "дала,,блог", []string{"дала", "блог"}},
		{"рет сақталады", "блог, дала", []string{"блог", "дала"}},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			got := parseTags(c.in)
			if !slices.Equal(got, c.want) {
				t.Errorf("parseTags(%q) = %v, күткеніміз %v", c.in, got, c.want)
			}
		})
	}
}

func TestReadingTime(t *testing.T) {
	cases := []struct {
		name  string
		words int
		want  int
	}{
		{"нөл", 0, 0},
		{"бір сөз", 1, 1},
		{"дәл екі жүз", 200, 1},
		{"екі жүз бір", 201, 2},
		{"дәл төрт жүз", 400, 2},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			if got := readingTime(c.words); got != c.want {
				t.Errorf("readingTime(%d) = %d, күткеніміз %d", c.words, got, c.want)
			}
		})
	}
}

func TestArticles(t *testing.T) {
	cases := []struct {
		name string
		path string
		code int
		body string
	}{
		{"мақала бар", "/read/dala", http.StatusOK, "Дала туралы"},
		{"мақала жоқ", "/read/joq", http.StatusNotFound, "мұндай мақала жоқ"},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			// Сервер көтерудің қажеті жоқ: өңдеуші — кәдімгі функция,
			// ал recorder браузер болып көрінеді.
			req := httptest.NewRequest(http.MethodGet, c.path, nil)
			rec := httptest.NewRecorder()

			articles().ServeHTTP(rec, req)

			if rec.Code != c.code {
				t.Errorf("код %d, күткеніміз %d", rec.Code, c.code)
			}
			if !strings.Contains(rec.Body.String(), c.body) {
				t.Errorf("жауапта %q жоқ, жауап: %q", c.body, rec.Body.String())
			}
		})
	}
}

Іске қосу:

$ go test -v -run TestParseTags
=== RUN   TestParseTags
=== RUN   TestParseTags/бос
=== RUN   TestParseTags/біреу
=== RUN   TestParseTags/шеттегі_бос_орындар
=== RUN   TestParseTags/әртүрлі_регистр
=== RUN   TestParseTags/үтірлер_арасы_бос
=== RUN   TestParseTags/рет_сақталады
--- PASS: TestParseTags 
    --- PASS: TestParseTags/бос 
    --- PASS: TestParseTags/біреу 
    --- PASS: TestParseTags/шеттегі_бос_орындар 
    --- PASS: TestParseTags/әртүрлі_регистр 
    --- PASS: TestParseTags/үтірлер_арасы_бос 
    --- PASS: TestParseTags/рет_сақталады 
PASS
ok  	sabaq38

Үзінділерден іске қосу уақыты алынды: ол әркімде әртүрлі.

Талдау

Көшірменің орнына кесте

Жағдайлар құрылымдар тілімінде жатыр — жағдайға бір жол:

cases := []struct {
	name string
	in   string
	want []string
}{
	{"бос", "", nil},
	{"біреу", "дала", []string{"дала"}},
	{"шеттегі бос орындар", "  дала  ", []string{"дала"}},
}

Мұндағы құрылым атсыз: тип дәл бір жерде керек, оған ат ойлап табудың қажеті жоқ. Үш өріс үш сұраққа жауап береді: жағдайдың аты қандай, не береміз, нені күтеміз.

Жетінші жағдайды қосу — бір жол жазу. Он жол тексеруді көшіру емес, бір жол жазу. Кестені дәл сол үшін жақсы көреді: ол жағдай қосуды арзан етеді, демек жағдайлар көбейеді.

Елестетіп көріңіз. Қалталардағы он қағаздың орнына бір сатып алу тізімі. Тізімде бәрі бірден көрінеді, жол қосу оңай, әрі жолда ештеңе жоғалмайды.

t.Run: әр жағдайдың өз аты бар

t.Run(c.name, func(t *testing.T) { ... }) ішкі тестті іске қосады — тест ішіндегі, өз аты мен өз нәтижесі бар бөлек тест.

Шығуда аттар есептің бөлігіне айналғаны көрінеді: TestParseTags/әртүрлі_регистр. Бос орындарды Go астыңғы сызықпен алмастырады, атты пәрмен жолына беруге болатындай.

Аттарды адамша жазады: «шеттегі бос орындар», «3-жағдай» емес. Жарты жылдан кейін есепті сіз оқисыз, әрі «3-жағдай» сізге ештеңе айтпайды.

Ішкі тестте өз t-сы бар. Бұл көрінгеннен маңыздырақ: ішкі тесттегі t.Errorf осы жағдайды құлатады, ал қалғандары орындала береді, әрі сіз бір іске қосқанда барлық ақауды бірден білесіз, біріншісін ғана емес.

Сынғанда не көрінеді

readingTime-дағы дөңгелектеуді сындырайық — + 199-ды алып тастайық:

$ go test
--- FAIL: TestReadingTime 
    --- FAIL: TestReadingTime/бір_сөз 
        main_test.go:51: readingTime(1) = 0, күткеніміз 1
    --- FAIL: TestReadingTime/екі_жүз_бір 
        main_test.go:51: readingTime(201) = 1, күткеніміз 2
FAIL
exit status 1
FAIL	sabaq38

Мұның бәрі осының үшін істелген. Есеп былай дейді: бестен дәл екі жағдай сынды, міне олардың аттары, міне не алдық, не күттік. «Тест құлады» емес, «бір сөз бен екі жүз бір сөз дұрыс саналмайды» — әрі осы екі аттан жоғары қарай дөңгелектеу жоғалғаны бірден түсінікті.

Хабарламаның пішіміне назар аударыңыз: readingTime(1) = 0, күткеніміз 1. Онда кіріс те, алынған да, күтілген де бар. «Сәйкес келмеді» деген хабар мұны бермейді, әрі кодтың орнына оны жөндеуге тура келеді.

Ат бойынша бір жағдай

Жағдайдың аты бар болғандықтан, оны бөлек іске қосуға болады:

$ go test -run 'TestReadingTime/екі_жүз_бір' -v
=== RUN   TestReadingTime
=== RUN   TestReadingTime/екі_жүз_бір
--- PASS: TestReadingTime 
    --- PASS: TestReadingTime/екі_жүз_бір 
PASS
ok  	sabaq38

Қиғаш сызық тесттің атын ішкі тесттің атынан бөледі. Тест екі жүз болып, ал жөндейтіні біреу болғанда, бұл әр түзетуде минуттарды үнемдейді.

Өңдеушіні серверсіз тексереді

TestArticles веб-өңдеушіні тексереді, әрі бұл кезде ешқандай сервер көтерілмейді:

req := httptest.NewRequest(http.MethodGet, c.path, nil)
rec := httptest.NewRecorder()

articles().ServeHTTP(rec, req)

Go-дағы өңдеуші — екі аргументі бар кәдімгі функция. httptest.NewRequest сұраныс жасайды, httptest.NewRecorder — өңдеуші желінің орнына жауап жазатын «қағаз парағы». Шақырудан кейін rec.Code-та код, rec.Body-де дене жатады.

Бұл жылдам (порт та, күту де жоқ) әрі сенімді (бос емес болатын ештеңе жоқ). Тесттегі нағыз серверді тізбекті түгел — орамаларымен, cookie-імен және қайта бағыттауларымен — тексергенде httptest.NewServer арқылы көтереді.

-cover: тест астынан қанша жол өтті

$ go test -cover
coverage: 90.0% of statements
ok  	sabaq38

Тоқсан пайыз — бұл тест кезінде кемінде бір рет орындалған жолдардың үлесі. Пайдалы сан: ол тесттердің қайда мүлде қарамағанын көрсетеді.

Әрі ол — жаман мақсат. Жолды ештеңе тексермей орындауға болады: функцияны шақырыңыз да, нәтижеге қарамаңыз — қамту өседі, сапа жоқ. Жүз пайыздық қамтуға оңай жетеді, әрі ол бағдарламаның жұмыс істейтінін білдірмейді.

Оны баға ретінде емес, ақ дақтардың картасы ретінде пайдаланыңыз.

Нені тексереді, нені тексермейді

Қателесу оңай әрі байқамау қымбат жерді тексереді: шекараларды (нөл, бір, дәл екі жүз), бөтен енгізілгенді талдауды, құқықтарды, ақшаны, күнге қатыстының бәрін.

Компилятордың өзі тексеретінді тексермейді, әрі бөтен кітапханаларды тексермейді: database/sql сізсіз де тексерілген. Әрі беттің көрінісін жол-жолымен тексермейді — ол апта сайын өзгереді, ал тест әр үтірде құлайтын болады.

Бөлек: тест іске қосу ретіне де, сағаттағы уақытқа да, интернетке де тәуелді болмауға тиіс. Мұндай тест аптасына бір рет түсініксіз себеппен құлайды да, ақыры оны сөндіріп тастайды — онымен бірге пайдасын да.

Сабақ картасы

Сабақ картасы: жағдайлар кестесі, аттар және есеп

Өз сөзіңізбен айтыңыз

Қарамай, дауыстап немесе қағазға жауап беріңіз. Жауаптары — сабақтың соңында.

  1. Қатарынан жазылған бес if-пен салыстырғанда t.Run не береді?
  2. Өңдеушіні неге серверді көтермей тексеруге болады?
  3. Жүз пайыз қамту неге жаман мақсат?

Тапсырма

Міндетті. Блогқа кестелік тесттер жазыңыз: ParseTags-қа, ReadingTime-ға және /read/{slug} өңдеушісіне — «мақала бар» және «мақала жоқ» жағдайларымен. go test ./... өтетінін тексеріңіз, сосын бір функцияны әдейі сындырып, есепке қараңыз.

Мұның бәрі step-26 ішінде жасалған — өзіңіз жазғаннан кейін салыстырыңыз.

Қалауыңызша.

  • Қойма тестін қосыңыз: t.TempDir() өзі тазаланатын қалта береді — сонда дерекқор ашып, мақала салып, оны қайта алыңыз.
  • Тесттер тимейтін функцияны go test -coverprofile=c.out пен go tool cover -html=c.out арқылы табыңыз.
  • Қазір өтпейтін жағдай қосыңыз — мысалы, бір бос орыннан тұратын тег — әрі тестті емес, кодты түзетіңіз.

Блогта бұл қайда тұрады

Осы сабақтан бастап түзету қорқынышты болуын қояды: go test ./... бір секундта кеше жұмыс істегенді сындырған-сындырмағаныңызды айтады.

Қарыздар. Блогта тест әзірге аз, әрі олар сессияларға да, құқықтарға да тимейді. Жарыс тексеруі жоқ — ол -race-пен бірге горутиналар туралы сабақта пайда болады. Әрі әр тесттің алдында бірдей толатын тесттік дерекқор да бізде әзірге жоқ.

Жауаптар

Жауаптарды көрсету
  1. Әр жағдайға ат пен бөлек нәтиже. Құлау қай жағдай сәйкес келмегенін көрсетеді, қалғандары бұл кезде орындала береді, әрі ат бойынша бір жағдайды -run арқылы іске қосуға болады.
  2. Себебі өңдеуші — http.ResponseWriter мен *http.Request қабылдайтын кәдімгі функция. httptest оған желінің орнына сұраныс пен қағаз парағын береді; бұл үшін порт та, сервер де керек емес.
  3. Себебі қамту тексерілген тұжырымдарды емес, орындалған жолдарды санайды. Жолды нәтижесін тексермей орындауға болады — сан өседі, ал ақаулар өтіп кете береді. Бұл баға емес, ақ дақтардың картасы.

Дереккөздер

Мәтінде қате не теру қатесі кездессе, бізге айтыңыз

Тапсырманы тексеру

Алдымен VS Code-та шешіп, іске қосыңыз — редактор қатені сол жерде көрсетеді. Дайын шешімді осында қойыңыз. Тексеретін — модель: ол қатені атап көрсетеді, бірақ дайын жауапты бермейді.

Тексеру үшін кіру керек. Кіру

Пікірлер (0)

Әзірге пікір жоқ. Бірінші болыңыз.