Így tesztelheti a párhuzamos kódot teszteléssel/szinkronteszttel: Ugrás az 1.24-re

által Go12m2025/04/06
Read on Terminal Reader

Túl hosszú; Olvasni

Ez a bejegyzés elmagyarázza ennek a kísérletnek a motivációját, bemutatja, hogyan kell használni a synctest csomagot, és megvitatja annak lehetséges jövőjét.
featured image - Így tesztelheti a párhuzamos kódot teszteléssel/szinkronteszttel: Ugrás az 1.24-re
Go HackerNoon profile picture

A Go egyik aláírási funkciója a párhuzamos támogatás.Goroutins és csatornák egyszerű és hatékony primitívek a párhuzamos programok írásához.


Az egyidejű programok tesztelése azonban nehéz és hibás lehet.


A Go 1.24-ben bemutatunk egy új, kísérleti testing/synctest csomagot, amely támogatja a kísérleti kód tesztelését.

Ez a bejegyzés elmagyarázza ennek a kísérletnek a motivációját, bemutatja, hogyan kell használni a synctest csomagot, és megvitatja annak lehetséges jövőjét.szinkronizálás és tesztelés


A Go 1.24-ben a testing/synctest csomag kísérleti jellegű, és nem tartozik a Go kompatibilitási ígéret hatálya alá. Alapértelmezés szerint nem látható. Ahhoz, hogy használni tudja, a kódot a környezetében beállított GOEXPERIMENT=synctest kódmal kell összeállítania.

szinkronizálás és tesztelésGYIK=szinkronizálás

A párhuzamos programok tesztelése nehéz

Kezdjük egy egyszerű példával.


A context.AfterFunc függvény megszervezi, hogy egy függvényt a kontextus törlése után saját goroutine-jében hívjanak.context.AfterMűködésA munkavégzés után

funkció TestAfterFunc(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) calledCh := make(chan struct{}) // zárva, amikor az AfterFunc context.AfterFunc(ctx, func() { close(calledCh) }) // TODO: Azt állítja, hogy az AfterFunc nem hívott. cancel() // TODO: Azt állítja, hogy az AfterFunc hívott. } 
func TestAfterFunc(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) calledCh := make(chan struct{}) // zárva, amikor az AfterFunc context.AfterFunc(ctx, func() { close(calledCh) }) // TODO: Azt állítja, hogy az AfterFunc nem hívott. cancel() // TODO: Azt állítja, hogy az AfterFunc hívott. }

Ebben a tesztben két feltételt szeretnénk ellenőrizni: A függvényt a kontextus törlése előtt nem hívják, és a függvényt a kontextus törlése után hívják.

A az


Könnyedén ellenőrizhetjük, hogy a függvényt még nem hívták yet, de hogyan ellenőrizhetjük, hogy nem hívják?


és mégnem fog


Egy közös megközelítés az, hogy várjunk egy kis időt, mielőtt arra a következtetésre jutnánk, hogy egy esemény nem fog megtörténni.

// funcCalled jelzi, hogy a funkció hívásra került-e. funcCalled := func() bool { select { case <-calledCh: return true case <-time.After(10 * time.Millisecond): return false } } ha funcCalled() { t.Fatalf("AfterFunc funkció hívása a kontextus törlése előtt") } cancel() ha!funcCalled() { t.Fatalf("AfterFunc funkció nem hívott a kontextus törlése után") } 
// funcCalled jelzi, hogy a funkciót hívták-e. funcCalled := func() bool { select { case <-calledCh: return true case <-time.After(10 * time.Millisecond): return false } } ha funcCalled() { t.Fatalf("AfterFunc funkció hívása a kontextus törlése előtt") } cancel() ha!funcCalled() { t.Fatalf("AfterFunc funkció nem hívott a kontextus törlése után") }

Ez a teszt lassú: 10 milliszekundum nem sok idő, de sok tesztnél felhalmozódik.


Ez a teszt is homályos: 10 milliszekundum hosszú idő egy gyors számítógépen, de nem szokatlan, hogy több másodperces szüneteket látunk a megosztott és túlterhelt CI rendszereken.

Tovább


A tesztet lassabbá tehetjük, a tesztet lassabbá tehetjük, de nem tudjuk gyorsabbá és megbízhatóbbá tenni.

A tesztelési/szinktatási csomag bevezetése

A testing/synctest csomag megoldja ezt a problémát. lehetővé teszi számunkra, hogy ezt a tesztet egyszerűen, gyorsan és megbízhatóan írjuk le, anélkül, hogy bármilyen változást végeznénk a tesztelt kódban.

szinkronizálás és tesztelés


A csomag csak két funkciót tartalmaz: Run és Wait.ForrásKezdőlap » Kódok » Kódok


Run egy funkciót hív egy új goroutine-ben. Ez a goroutine és az általa elindított bármely goroutine egy elszigetelt környezetben létezik, amelyet buboréknak nevezünk. Wait arra vár, hogy minden goroutine az aktuális goroutine buborékában blokkoljon egy másik goroutine-t a buborékban.

Forrás buborékKezdőlap » Kódok » Kódok


Vegyük át a fenti tesztünket a testing/synctest csomag használatával.

szinkronizálás és tesztelés
funkció TestAfterFunc(t *testing.T) { synctest.Run(func() { ctx, cancel := context.WithCancel(context.Background()) funcCalled := false context.AfterFunc(ctx, func() { funcCalled = true }) synctest.Wait() ha funcCalled { t.Fatalf("AfterFunc funkció hívott, mielőtt a kontextus törlődik") } cancel() synctest.Wait() ha!funcCalled { t.Fatalf("AfterFunc funkció nem hívott, miután a kontextus törlődik") }) } funkció TestAfterFunc(t *testing.T) { synctest.Run(func() { ctx, cancel := context.WithCancel(context.Background()) funcCalled := false context.AfterFunc(ctx, func() { funcCalled = true }) synctest.Wait() ha funcCalled { t.Fatalf("AfterFunc funkció hívott, mielőtt a kontextus törlődik") } cancel() synctest.Wait() ha!funcCalled { t.Fatalf("AfterFunc funkció nem hívott, miután a kontextus törlődik") } } } } 

Ez majdnem azonos az eredeti tesztünkkel, de a tesztet egy synctest.Run hívásba csomagoltuk, és a funkció hívása előtt synctest.Wait hívunk.

szinkronizálás és futtatásKezdőlap » Kezdőlap » Kezdőlap » Kód


A Wait függvény arra vár, hogy a hívó buborékában minden goroutine blokkoljon. Amikor visszatér, tudjuk, hogy a kontextuscsomag vagy felhívta a függvényt, vagy nem hívja meg, amíg nem teszünk további lépéseket.

Kezdőlap » Kódok » Kódok


Ez a teszt mostantól gyors és megbízható.


Korábban egy csatornát kellett használnunk, hogy elkerüljük a tesztgoroutine és a AfterFunc goroutine közötti adatversenyt, de a Wait funkció most biztosítja ezt a szinkronizációt.


Kezdőlap » Kódok » KódokA munkavégzés utánKezdőlap » Kódok » Kódok


A versenyérzékelő megérti a Wait hívásokat, és ez a teszt akkor halad át, ha a -race segítségével fut. Ha eltávolítjuk a második Wait hívást, a versenyérzékelő helyesen jelent egy adatversenyt a tesztben.

Kezdőlap » Kódok » KódokKezdőlap » Kódok » KódokKezdőlap » Kódok » Kódok

Tesztelési idő

A versengő kód gyakran foglalkozik az idővel.


Az idővel működő kód tesztelése nehéz lehet.A valós idejű tesztek használata lassú és homályos teszteket eredményez, amint azt fentebb láttuk.A hamis idő használata megköveteli a time csomagfunkciók elkerülését, és a tesztelés alatt álló kód megtervezését, hogy egy opcionális hamis órával működjön.

idő


A testing/synctest csomag megkönnyíti az időt használó kód tesztelését.

szinkronizálás és tesztelés


A buborékban a Run által indított golyók hamis órát használnak. A buborékban a time csomag funkciói a hamis órán működnek.Forrás idő


A demonstrációhoz írjunk egy tesztet a context.WithTimeout funkcióra. WithTimeout létrehoz egy kontextus gyermekeit, amely egy adott időzítés után lejár.

context.WithTimeout ÖsszefüggésKezdőlap » Kódok » Kódok
funkció TestWithTimeout(t *testing.T) { synctest.Run(func() { const timeout = 5 * time.Second ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() // Wait csak kevesebb, mint a timeout. time.Sleep(timeout - time.Nanosecond) synctest.Wait() ha err : ctx.Err(); err!= nil { t.Fatalf("before timeout, ctx.Err() = %v; want nil", err) } // Wait a többi úton, amíg a timeout time.Sleeptime.Sleeptime(Nanosecond) synctest.Wait() ha err : ct= ctx.Err(),func TestWithTimeout(t *testing.T) { synctest.Run(func() { const timeout = 5 * time.Second ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() // Wait csak kevesebb, mint a timeout. time.Sleep(timeout - time.Nanosecond) synctest.Wait() ha err := ctx.Err(); err!= nil { t.Fatalf("before timeout, ctx.Err() = %v; want nil", err) } // Wait a többi út, amíg a timeout time.Sleeptime(Nanosecond) synctest.Wait() ha : err= ctx.Err(); err!= context.DeadlineExAz egyetlen különbség az, hogy a tesztfunkciót a synctest.Run-ba csomagoljuk, és minden egyes time.Sleep-hívás után hívjuk a synctest.Wait-t, hogy várjuk a kontextuscsomag időzítőit a futás befejezéséig.

szinkronizálás és futtatásKezdőlap » Kezdőlap » Kezdőlap » Kód idő.alvás

Blocking és a buborék

A testing/synctest egyik legfontosabb koncepciója az, hogy a buborék tartósan blokkolva lesz

. Ez akkor történik meg, amikor a buborék minden goroutine blokkolva van, és csak egy másik goroutine képes feloldani a buborék blokkolását.


szinkronizálás és tesztelés tartósan blokkolva


Amikor egy buborék tartósan blokkolva van:

  • Ha van egy kilépő Wait hívás, akkor visszatér.
  • Ellenkező esetben az idő előrehalad a következő alkalommal, amely feloldhatja a goroutine, ha van.
  • Ellenkező esetben a buborék zárva van, és Run pánik.
Ha van egy kiemelkedő Wait hívás, akkor visszatér.Kezdőlap » Kódok » KódokEllenkező esetben az idő előrehalad a következő alkalomra, amely feloldhatja a goroutint, ha van.Ellenkező esetben a buborék le van zárva, és a Run pánikba esik.Forrás


Egy buborék nem tartósan blokkolva, ha bármilyen goroutin blokkolva van, de felébredhet valamilyen esemény a buborékon kívülről.


Az olyan műveletek teljes listája, amelyek tartósan blokkolják a goroutint:

  • a küldés vagy fogadás egy nil csatornán
  • a küldés vagy fogadás blokkolva egy ugyanabban a buborékban létrehozott csatornán
  • egy olyan kijelentés, amelyben minden eset tartósan blokkolja
  • time.Sleep
  • sync.Cond.Wait
  • sync.WaitGroup.Wait
  • a küld vagy fogad egy nil csatornán
  • a küldés vagy fogadás blokkolva van egy ugyanabban a buborékban létrehozott csatornán
  • egy kiválasztott mondat, amelyben minden eset tartósan blokkolódik
  • idő.Alvás
  • idő.alvás
  • sync.Cond.Wait
  • sync.Cond.Wait szinkronizálás
  • sync.WaitGroup.Wait
  • sync.WaitGroup.Wait Megtekintés

    Mutasítások

    A sync.Mutex műveletek nem tartósan blokkolnak.

    sync.Mutex szinkronizálása


    Gyakori, hogy a funkciók globális mutex-t szereznek meg. Például a tükröződő csomag számos funkciója egy mutex által védett globális gyorsítótárat használ. Ha egy goroutin egy synctest buborékban blokkol, miközben egy goroutin által a buborékon kívül tartott mutexet szerez, akkor nem tartósan blokkolva van – blokkolva van, de a buborékon kívülről egy goroutin feloldja a blokkolását.


    Mivel a mutexeket általában nem tartják hosszú ideig, egyszerűen kizárjuk őket a testing/synctest megfontolásából.

    szinkronizálás és tesztelés

    Hálózati csatornák

    A buborékban létrehozott csatornák eltérően viselkednek, mint a külsőben létrehozott csatornák.


    A csatorna-műveletek csak akkor blokkolnak tartósan, ha a csatorna buborékos (a buborékban keletkezik).


    Ezek a szabályok biztosítják, hogy a goroutin csak akkor kerül tartósan blokkolásra, ha a buborékban lévő goroutinokkal kommunikál.

    I / O

    A külső I/O műveletek, például a hálózati kapcsolaton keresztüli olvasás nem blokkolják tartósan.


    A hálózati olvasmányokat a buborékon kívülről, esetleg más folyamatokból származó írások is feloldhatják. Még akkor is, ha a hálózati kapcsolat egyetlen írója ugyanabban a buborékban van, a futási idő nem tudja megkülönböztetni a kapcsolatot, amely több adat megérkezését várja, és azt, amelyben a kernel adatokat kapott, és folyamatban van.


    A hálózati kiszolgáló vagy kliens tesztelése a synctest segítségével általában hamis hálózati végrehajtást igényel. Például a net.Pipe funkció egy pár net.Conn-ot hoz létre, amelyek a memóriában lévő hálózati kapcsolatot használják, és a synctestben használhatók.

    net.Pipe ésnet.Conn Hasonlóképpen

    Bubble élettartam

    A Run funkció elindítja a goroutint egy új buborékban. Visszatér, amikor a buborékban minden goroutint elhagyott. pánikba esik, ha a buborék tartósan blokkolva van, és az idő előrehaladásával nem lehet feloldani.

    Forrás


    A követelmény, hogy minden goroutine a buborék kijárat előtt a futás visszatér azt jelenti, hogy a tesztek kell óvatosan tisztítsa meg a háttér goroutine befejezése előtt.

    Hálózati kód tesztelése

    Vessünk egy másik példát, ezúttal a testing/synctest csomagot használva egy hálózati program tesztelésére.szinkronizálás és teszteléshálózat / HTTP


    A kérést küldő HTTP kliens tartalmazhat egy „Várj: 100-folytatás” fejlécet, amely megmondja a kiszolgálónak, hogy a kliensnek további adata van küldeni.A kiszolgáló 100 Folytatás információs válaszsal válaszolhat a kérés többi részének megkeresésére, vagy egy másik állapotgal, amely megmondja a kliensnek, hogy a tartalom nem szükséges. Például egy nagy fájlt feltöltő kliens használhatja ezt a funkciót annak megerősítésére, hogy a kiszolgáló hajlandó elfogadni a fájlt a küldés előtt.


    A tesztünk megerősíti, hogy a „Várj: 100-folyton” fejléc elküldésekor a HTTP-ügyfél nem küldi el a kérés tartalmát, mielőtt a kiszolgáló kéri, és hogy a tartalom 100 Folytatott válasz után küldi el.


    Gyakran egy kommunikáló kliens és szerver tesztelése loopback hálózati kapcsolatot használhat.A testing/synctest használatakor azonban általában egy hamis hálózati kapcsolatot szeretnénk használni, amely lehetővé teszi számunkra, hogy észleljük, mikor minden goroutine blokkolva van a hálózaton.szinkronizálás és tesztelésHTTP Szállítmányozásnet.Pipe és

    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, hálózat, címsor) (net.Conn, hiba) { return cliConn, nil }, // egy nem-zéró időtávolság beállítása lehetővé teszi a "Expect: 100-continue" kezelést. // Mivel a következő teszt nem alszik, // soha nem találkozunk ezzel az időtávolsággal, // még akkor is, ha a teszt hosszú ideig tart egy lassú gépen. ExpectContinueTimeoutfunc 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 }, // A nem-zéró időzítés beállítása lehetővé teszi a "Expect: 100-continue" kezelést. // Mivel a következő teszt nem alszik, // soha nem találkozunk ezzel az időzítéssel, // még akkor is, ha a teszt hosszú ideig tart egy lassú gépen.


    Ezen a szállításon kérést küldünk a „Remélem: 100 folyamatos” fejléc-készlettel.A kérést új goroutine-ben küldjük el, mivel a teszt végéig nem fejeződik be.

     test := "kérj testet" menjen func() { req, _ := http.NewRequest("PUT", "http://test.tld/", strings.NewReader(body)) req.Header.Set("Expect", "100-folytatás") resp, err := tr.RoundTrip(req) ha err!= nil { t.Errorf("RoundTrip: unexpected error %v", err) } else resp {.Body.Close() }() 
    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) ha err!= nil { t.Errorf("RoundTrip: unexpected error %v", err) } egyéb { resp.Body.Close() }()


    Elolvashatjuk az ügyfél által küldött kérési fejléceket.

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


    Most a teszt középpontjába kerülünk. szeretnénk kijelenteni, hogy az ügyfél még nem küldi el a kérelmet.


    Elkezdünk egy új goroutint másolni a szerverre küldött testet egy strings.Builder-ba, várjuk, hogy a buborékban lévő összes goroutint blokkoljuk, és ellenőrizzük, hogy még nem olvastunk semmit a testből.

    strings.Builder beállítása


    Ha elfelejtjük a synctest.Wait hívást, a versenyérzékelő helyesen panaszkodik egy adatversenyre, de a Wait hívással ez biztonságos.

    Kezdőlap » Kezdőlap » Kezdőlap » KódKezdőlap » Kódok » Kódok
     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) } 
    var gotBody strings.Builder go io.Copy(&gotBody, req.Body) synctest.Wait() ha kapott := gotBody.String(); kapott!= "" { t.Fatalf("előtte küldött 100 Folytatás, váratlanul olvasott test: %q", kapott) }


    Írunk egy „100 Folytatás” válaszot az ügyfélnek, és ellenőrizzük, hogy most elküldi a kérelmet.

     srvConn.Write([]byte("HTTP/1.1 100 Continue\r\r\n\n")) synctest.Wait() ha kapott := gotBody.String(); kapott!= test { t.Fatalf("szállítás után 100 Folytatás, olvassa el a test %q, akarja %q", kapott, test) } 
    srvConn.Write([]byte("HTTP/1.1 100 Continue\r\n\n\n")) synctest.Wait() ha kapott := gotBody.String(); kapott!= test { t.Fatalf("szállítás után 100 Folytatás, olvassa el test %q, akar %q", kapott, test) }

    És végül a „200 OK” válasz küldésével fejezzük be a kérést.


    A synctest.Run hívás megvárja, hogy mindannyian kilépjenek, mielőtt visszatérnének.


    szinkronizálás és futtatás
     srvConn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n") }) } 
    srvConn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n") }) }

    Ez a teszt könnyen kiterjeszthető más viselkedések tesztelésére, például annak ellenőrzésére, hogy a kérelmet nem küldi el, ha a szerver nem kéri, vagy hogy küldi el, ha a szerver nem válaszol időtartam alatt.

    A kísérlet állapota

    Mi bevezetjük a testing/synctest a Go 1.24-ben, mint egy experimentális csomagot.A visszajelzésektől és a tapasztalattól függően kiadhatjuk módosításokkal vagy anélkül, folytathatjuk a kísérletet, vagy eltávolíthatjuk a Go jövőbeli verziójában.

    szinkronizálás és teszteléskísérletezés


    A csomag alapértelmezés szerint nem látható. Ahhoz, hogy használhassa, összeállítsa a kódot a környezetében található GOEXPERIMENT=synctest beállítással.

    GYIK=szinkronizálás


    Ha kipróbálod a testing/synctest lehetőséget, kérjük, jelentsd a pozitív vagy negatív tapasztalataidat a go.dev/issue/67434 címen.szinkronizálás és tesztelésgo.dev/issue/67434» HR

    Hitelek: Damien Neil

    Hitelek :Főszerepben Damien Neil


    Photo by Gabriel Gusmao on Unsplash

    Fotó: Gabriel Gusmao on UnsplashGabriel GusmaoUnsplash


    Ez a cikk elérhető a The Go Blog egy CC BY 4.0 DEED licenc alatt.

    Ez a cikk elérhető a The Go Blog egy CC BY 4.0 DEED licenc alatt.

    A Go BlogA Go Blog


    Trending Topics

    blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks