102 оқулар

Тақырып: How to Test Concurrent Code with Testing/Synctest: Go 1.24

бойынша Go12m2025/04/06
Read on Terminal Reader

Тым ұзақ; Оқу

Бұл пост осы эксперименттің мотивациясын түсіндіреді, синктест пакетін қалай пайдалану керектігін көрсетеді және оның әлеуетті болашағын талқылайды.
featured image - Тақырып: How to Test Concurrent Code with Testing/Synctest: Go 1.24
Go HackerNoon profile picture

Go қолданбасының қолтаңба мүмкіндіктерінің бірі - параллельділікке кірістірілген қолдау. Горутиндер мен арналар қатарлас бағдарламаларды жазуға арналған қарапайым және тиімді примитивтер болып табылады.


Дегенмен, қатар жүретін бағдарламаларды тестілеу қиын және қате болуы мүмкін.


Go 1.24 нұсқасында біз параллельдік кодты сынауға қолдау көрсету үшін жаңа, эксперименттік testing/synctest пакетін енгіземіз. Бұл пост осы эксперименттің мотивациясын түсіндіреді, синктест пакетін қалай пайдалану керектігін көрсетеді және оның ықтимал болашағын талқылайды.


Go 1.24 нұсқасында testing/synctest бумасы эксперименталды және Go үйлесімділік уәдесіне бағынбайды. Ол әдепкі бойынша көрінбейді. Оны пайдалану үшін ортаңыздағы GOEXPERIMENT=synctest жиынымен кодты құрастырыңыз.

Бір мезгілде жұмыс істейтін бағдарламаларды тестілеу қиын

Алдымен қарапайым мысалды қарастырайық.


context.AfterFunc функциясы мәтінмән жойылғаннан кейін функцияның өзінің жеке горитинде шақырылуын реттейді. Міне, AfterFunc үшін ықтимал сынақ:

 func TestAfterFunc(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) calledCh := make(chan struct{}) // closed when AfterFunc is called context.AfterFunc(ctx, func() { close(calledCh) }) // TODO: Assert that the AfterFunc has not been called. cancel() // TODO: Assert that the AfterFunc has been called. }

Бұл сынақта біз екі шартты тексергіміз келеді: Функция мәтінмән жойылмай тұрып шақырылмайды және функция контекст жойылғаннан кейін шақырылады .


Бір мезгілде жүйеде теріс мәнді тексеру қиын. Біз функцияның әлі шақырылмағанын оңай тексере аламыз, бірақ оның шақырылмайтынын қалай тексереміз?


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

 // funcCalled reports whether the function was called. funcCalled := func() bool { select { case <-calledCh: return true case <-time.After(10 * time.Millisecond): return false } } if funcCalled() { t.Fatalf("AfterFunc function called before context is canceled") } cancel() if !funcCalled() { t.Fatalf("AfterFunc function not called after context is canceled") }

Бұл сынақ баяу: 10 миллисекунд көп уақыт емес, бірақ ол көптеген сынақтарды қосады.


Бұл сынақ та қабыршақ: 10 миллисекунд жылдам компьютерде ұзақ уақыт, бірақ ортақ және шамадан тыс жүктелген CI жүйелерінде бірнеше секундқа созылатын үзілістерді көру әдеттен тыс емес.


Біз сынақты баяулау есебінен аз қабыршақтай аламыз және оны қабыршақтау есебінен оны баяулатамыз, бірақ біз оны жылдам әрі сенімді ете алмаймыз.

Тестілеу/синктест пакетімен таныстыру

testing/synctest пакеті бұл мәселені шешеді. Бұл сынақтан өтіп жатқан кодқа ешбір өзгеріссіз қарапайым, жылдам және сенімді болу үшін осы сынақты қайта жазуға мүмкіндік береді.


Бумада тек екі функция бар: Run және Wait .


Run жаңа горутиндегі функцияны шақырады. Бұл горутин және ол бастаған кез келген горутиндер оқшауланған ортада болады, оны біз көпіршік деп атаймыз. Ағымдағы горутин көпіршігідегі әрбір горутин көпіршіктегі басқа горутинге бөгет болғанша Wait .


Жоғарыдағы сынақты testing/synctest бумасын пайдаланып қайта жазайық.

 func TestAfterFunc(t *testing.T) { synctest.Run(func() { ctx, cancel := context.WithCancel(context.Background()) funcCalled := false context.AfterFunc(ctx, func() { funcCalled = true }) synctest.Wait() if funcCalled { t.Fatalf("AfterFunc function called before context is canceled") } cancel() synctest.Wait() if !funcCalled { t.Fatalf("AfterFunc function not called after context is canceled") } }) }

Бұл біздің бастапқы тестімізге дерлік ұқсайды, бірақ біз сынақты synctest.Run Іске қосу қоңырауына орап қойдық және функцияның шақырылғанын немесе шақырылмағанын растамас бұрын synctest.Wait деп атаймыз.


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


Бұл сынақ енді жылдам әрі сенімді.


Сынақ оңайырақ: біз calledCh арнасын логикалық мәнге ауыстырдық. Бұрын біз сынақ горутині мен AfterFunc горутині арасындағы деректер жарысын болдырмау үшін арнаны пайдалануымыз керек еді, бірақ Wait функциясы енді сол синхрондауды қамтамасыз етеді.


Жарыс детекторы Wait қоңырауларын түсінеді және бұл сынақ -race арқылы іске қосылғанда өтеді. Екінші Wait қоңырауын алып тастасақ, жарыс детекторы сынақтағы деректер жарысы туралы дұрыс хабарлайды.

Тестілеу уақыты

Бір уақыттағы код көбінесе уақытпен айналысады.


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


testing/synctest пакеті уақытты пайдаланатын кодты тексеруді жеңілдетеді.


Run іске қосқан көпіршіктегі горутиндер жалған сағатты пайдаланады. Көпіршік ішінде time бумасындағы функциялар жалған сағатта жұмыс істейді. Барлық горутиндер бұғатталған кезде көпіршікте уақыт ілгерілейді.


Көрсету үшін context.WithTimeout функциясына тест жазайық. WithTimeout мәтінмәннің еншілесін жасайды, оның мерзімі берілген күту уақытынан кейін аяқталады.

 func TestWithTimeout(t *testing.T) { synctest.Run(func() { const timeout = 5 * time.Second ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() // Wait just less than the timeout. time.Sleep(timeout - time.Nanosecond) synctest.Wait() if err := ctx.Err(); err != nil { t.Fatalf("before timeout, ctx.Err() = %v; want nil", err) } // Wait the rest of the way until the timeout. time.Sleep(time.Nanosecond) synctest.Wait() if err := ctx.Err(); err != context.DeadlineExceeded { t.Fatalf("after timeout, ctx.Err() = %v; want DeadlineExceeded", err) } }) }

Біз бұл тестті нақты уақытпен жұмыс істегендей жазамыз. Жалғыз айырмашылық мынада, біз сынақ функциясын synctest.Run ішінде орап аламыз және synctest.Wait time.Sleep кейін күте тұрамыз. Мәтінмәндік буманың таймерлерінің іске қосылуын аяқтау үшін күту үшін ұйқы қоңырауы.

Блоктау және көпіршік

testing/synctest негізгі тұжырымдамасы көпіршіктің ұзақ уақытқа бітеліп қалуы болып табылады. Бұл көпіршіктегі әрбір горутин бітеліп қалғанда орын алады және оны тек көпіршіктегі басқа горутин бұғаттаудан шығаруға болады.


Көпіршік ұзақ уақыт бітеліп қалса:

  • Егер күтпеген Wait болса, ол қайтарылады.
  • Әйтпесе, уақыт, егер бар болса, горутинді блоктан шығаруы мүмкін келесі уақытқа жылжиды.
  • Әйтпесе, көпіршік тығырыққа тіреледі және Run дүрбелеңі болады.


Кез келген горутин бітеліп қалса, көпіршік ұзақ уақыт бітеліп қалмайды, бірақ көпіршік сыртындағы қандай да бір оқиға оятуы мүмкін.


Горутинді тұрақты түрде блоктайтын операциялардың толық тізімі:

  • нөл арнасында жіберу немесе алу
  • бір көпіршік ішінде жасалған арнада блокталған жіберу немесе қабылдау
  • таңдау мәлімдемесі, онда әрбір жағдай тұрақты түрде блокталады
  • time.Sleep
  • sync.Cond.Wait
  • sync.WaitGroup.Wait

Мутекстер

sync.Mutex әрекеттер ұзаққа созылмайды.


Функциялардың ғаламдық мутекс алуы жиі кездеседі. Мысалы, рефлексия бумасындағы бірқатар функциялар мутекс арқылы қорғалған жаһандық кэшті пайдаланады. Егер көпіршіктің сыртындағы горутин ұстаған мутексті алу кезінде синктесті көпіршіктегі горутин блоктаса, ол ұзақ уақыт бітеліп қалмайды — ол бұғатталған, бірақ оның көпіршігі сыртындағы горутин арқылы блоктан шығарылады.


Мутекстер әдетте ұзақ уақыт бойы ұсталмайтындықтан, біз оларды жай ғана testing/synctest қараудан шығарамыз.

Арналар

Көпіршік ішінде жасалған арналар сырттан жасалған арналардан басқаша әрекет етеді.


Арнаның жұмысы тек арна көпіршіктелген (көпіршікте жасалған) болса ғана тұрақты түрде блокталады. Көпіршікті сырттан көпіршікті арнада жұмыс істеу.


Бұл ережелер горутиннің көпіршігі ішінде горутиндермен байланысқан кезде ғана берік блокталуын қамтамасыз етеді.

енгізу/шығару

Желі қосылымынан оқу сияқты сыртқы енгізу/шығару операциялары ұзақ уақытқа блокталмайды.


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


Желілік серверді немесе клиентті синктестпен сынау әдетте жалған желіні енгізуді талап етеді. Мысалы, net.Pipe функциясы жадтағы желі қосылымын пайдаланатын және синктест сынақтарында пайдалануға болатын net.Conn жұбын жасайды.

Көпіршікті пайдалану мерзімі

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


Іске қосу қайтару алдында көпіршіктегі әрбір горутиннің шығу талабы сынақтарды аяқтамас бұрын кез келген фондық горутиндерді тазалау үшін мұқият болу керектігін білдіреді.

Желілік кодты сынау

Басқа мысалды қарастырайық, бұл жолы желілік бағдарламаны сынау үшін testing/synctest пакетін пайдалану. Бұл мысал үшін біз net/http бумасының 100 Жалғастыру жауабын өңдеуін тексереміз.


Сұрауды жіберетін HTTP клиенті серверге клиентте жіберу үшін қосымша деректер бар екенін айту үшін «Күту: 100-жалғастыру» тақырыбын қамтуы мүмкін. Содан кейін сервер сұраудың қалған бөлігін сұрау үшін 100 Жалғастыру ақпараттық жауабымен немесе клиентке мазмұнның қажет емес екенін айту үшін басқа күймен жауап бере алады. Мысалы, үлкен файлды жүктеп салатын клиент бұл мүмкіндікті сервер файлды жібермес бұрын қабылдауға дайын екенін растау үшін пайдалана алады.


Біздің тестіміз «Күту: 100-жалғастыру» тақырыбын жіберген кезде HTTP клиенті сервер сұрағанға дейін сұрау мазмұнын жібермейтінін және 100 Жалғастыру жауабын алғаннан кейін мазмұнды жіберетінін растайды.


Көбінесе байланысатын клиент пен сервердің сынақтары кері желі қосылымын пайдалана алады. Дегенмен, testing/synctest жұмыс істегенде, біз әдетте желіде барлық горутиндердің қашан блокталғанын анықтауға мүмкіндік беру үшін жалған желі қосылымын пайдаланғымыз келеді. Бұл сынақты net.Pipe арқылы жасалған жадтағы желі қосылымын пайдаланатын http.Transport (HTTP клиенті) жасау арқылы бастаймыз.

 func Test(t *testing.T) { synctest.Run(func() { srvConn, cliConn := net.Pipe() defer srvConn.Close() defer cliConn.Close() tr := &http.Transport{ DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { return cliConn, nil }, // Setting a non-zero timeout enables "Expect: 100-continue" handling. // Since the following test does not sleep, // we will never encounter this timeout, // even if the test takes a long time to run on a slow machine. ExpectContinueTimeout: 5 * time.Second, }


Біз осы көлікке сұрауды «Күту: 100-жалғастыру» тақырыбы жинағымен жібереміз. Сұрау сынақтың соңына дейін аяқталмайтындықтан, жаңа горутинде жіберіледі.

 body := "request body" go func() { req, _ := http.NewRequest("PUT", "http://test.tld/", strings.NewReader(body)) req.Header.Set("Expect", "100-continue") resp, err := tr.RoundTrip(req) if err != nil { t.Errorf("RoundTrip: unexpected error %v", err) } else { resp.Body.Close() } }()


Біз клиент жіберген сұрау тақырыптарын оқимыз.

 req, err := http.ReadRequest(bufio.NewReader(srvConn)) if err != nil { t.Fatalf("ReadRequest: %v", err) }


Енді біз сынақтың мәніне келдік. Клиент сұраныстың негізгі бөлігін әлі жібермейтінін айтқымыз келеді.


Біз серверге жіберілген денені strings.Builder ішіне көшіретін жаңа горутинді бастаймыз, көпіршіктегі барлық горутиндердің блокталуын күтеміз және денеден әлі ештеңе оқымағанымызды тексереміз.


synctest.Wait қоңырауын ұмытсақ, жарыс детекторы деректер жарысы туралы дұрыс шағымданады, бірақ Wait арқылы бұл қауіпсіз.

 var gotBody strings.Builder go io.Copy(&gotBody, req.Body) synctest.Wait() if got := gotBody.String(); got != "" { t.Fatalf("before sending 100 Continue, unexpectedly read body: %q", got) }


Біз клиентке «100 жалғастыру» жауабын жазамыз және оның қазір сұраудың негізгі бөлігін жіберетінін тексереміз.

 srvConn.Write([]byte("HTTP/1.1 100 Continue\r\n\r\n")) synctest.Wait() if got := gotBody.String(); got != body { t.Fatalf("after sending 100 Continue, read body %q, want %q", got, body) }

Соңында сұрауды аяқтау үшін «200 OK» жауабын жіберу арқылы аяқтаймыз.


Осы сынақ кезінде біз бірнеше горутиндерді бастадық. synctest.Run қоңырауы қайтып оралмас бұрын олардың барлығының шығуын күтеді.

 srvConn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n")) }) }

Бұл сынақты басқа әрекеттерді тексеру үшін оңай кеңейтуге болады, мысалы, егер сервер сұрамаса, сұрау органының жіберілмейтінін немесе сервер күту уақытында жауап бермесе, жіберілетінін тексеру.

Эксперимент жағдайы

Біз Go 1.24 жүйесінде testing/synctest эксперименттік пакет ретінде енгіземіз. Кері байланыс пен тәжірибеге байланысты біз оны түзетулермен немесе түзетусіз шығаруымыз, экспериментті жалғастыруымыз немесе Go қолданбасының болашақ нұсқасында жоюымыз мүмкін.


Пакет әдепкі бойынша көрінбейді. Оны пайдалану үшін ортаңыздағы GOEXPERIMENT=synctest жиынымен кодты құрастырыңыз.


Пікіріңізді тыңдағымыз келеді! testing/synctest қолданып көрсеңіз, go.dev/issue/67434 сайтында оң немесе теріс тәжірибелеріңізді хабарлаңыз.


Несие: Дэмиен Нил


Unsplash сайтындағы Габриэль Гусмаоның суреті


Бұл мақала қол жетімді Go блогы CC BY 4.0 DEED лицензиясы бойынша.


L O A D I N G
. . . comments & more!

About Author

Go HackerNoon profile picture
Go@Go
Go is an open-source programming language used by companies such as Google, Meta, Microsoft, and more.

ТЕГТЕРДІ АЛУ

БҰЛ МАҚАЛА БАСҚАРҒАН...

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks