Testa samtidiga Go-kod med synctest
Sluta använda sömn i samtidiga Go-tester.
Att testa sammanparalliserad Go-kod har alltid krävt en viss disciplin. Goroutiner är billiga, kanaler är enkla och avbrott via context är idiomatic — bakgrundsgoroutiner och timrar finns överallt i verkliga Go-tjänster.
Men att testa allt detta pålitligt är svårare än att skriva själva koden.

Det vanliga dåliga mönstret är bekant:
go doSomething()
time.Sleep(100 * time.Millisecond)
if !done {
t.Fatal("background work did not finish")
}
Detta test kan fungera på din laptop och misslyckas i CI. Eller så kan det fungera i sex månader och sedan misslyckas på en belastad körare. Eller så kan det vara långsamt eftersom någon ökat sömnperioden från 100 millisekunder till 2 sekunder “för att vara på den säkra sidan”.
Detta är inte bra testning — det är att spela hasard med en timer, och den insatsen blir dyrare när testsuiten växer.
Paketet testing/synctest ger Go-utvecklare ett bättre sätt att testa många former av asynkron och tidsberoende kod. Det låter ett test köras i en isolerad bubbla, ger bubblan en falsk klocka och tillhandahåller ett sätt att vänta tills goroutiner inuti bubblan är blockerade.
Resultatet är enkelt men kraftfullt:
- Inga godtyckliga sömnperioder
- Snabbare timeout-tester
- Mer deterministiska parallella tester
- Bättre testning av context-avbrott
- Bättre testning av bakgrundsgoroutiner
- Mindre flaky CI
Den något åsiktsstyrda versionen: om din parallella Go-test beror på en verklig time.Sleep, bör du troligen behandla den testen som misstänkt.
Vad testing/synctest är
testing/synctest är ett Go-standardbibliotekspaket för att testa parallell kod.
Det tillhandahåller två huvudfunktioner:
package synctest
func Test(t *testing.T, f func(*testing.T))
func Wait()
synctest.Test kör en funktion inuti en isolerad testbubbla. Alla goroutiner som startas inuti den bubblan är också en del av bubblan, tiden inuti bubblan är falsk, och paketet time arbetar mot den falska klockan snarare än den verkliga väggklockan.
synctest.Wait väntar tills alla andra goroutiner i bubblan är varaktigt blockerade. Det låter abstrakt, men den praktiska effekten är lätt att förstå:
synctest.Test(t, func(t *testing.T) {
time.Sleep(10 * time.Second)
})
Detta får inte ditt test att vänta 10 verkliga sekunder. Inuti synctest-bubblan kan tiden avancera omedelbart när bubblan är blockerad och väntar på att tiden ska gå framåt — det är kärntricket bakom paketet.
Varför parallella Go-tester är flaky
Om du är ny till Go-testning i allmänhet, täcker Go Unit Testing: Structure & Best Practices testpaketet, tabelldrivna tester och mock-mönster som utgör grunden som denna artikel bygger på. Parallella tester är oftast flaky av en av tre anledningar.
För det första beror de på schemaläggaren. En goroutine kan köras omedelbart på din maskin och senare i CI.
För det andra beror de på verklig tid. Ett test som sover i 50 millisekunder utgår från att 50 millisekunder är tillräckligt tid för att bakgrundsarbetet ska slutföras.
För det tredje observerar de för tidigt tillstånd. Testet kontrollerar resultatet innan bakgrundsarbetet faktiskt har slutförts.
Här är ett enkelt exempel:
func TestBackgroundWorkBad(t *testing.T) {
done := false
go func() {
done = true
}()
time.Sleep(10 * time.Millisecond)
if !done {
t.Fatal("background work did not finish")
}
}
Detta test har två problem.
Det uppenbara är sömnperioden. Det finns ingen garanti för att 10 millisekunder är rätt mängd tid.
Det mindre uppenbara är data race. Testet skriver done i en goroutine och läser den i en annan utan synkronisering.
Du kan fixa detta specifika exempel med en kanal eller en sync.WaitGroup, och ofta bör du göra det. Men när koden som testas använder timrar, context-deadlines, time.AfterFunc, bakgrundsarbetare eller fördröjd rengöring, kan testet fortfarande bli klumpigt — och det är exakt där testing/synctest hjälper till.
Idén: kör testet inuti en bubbla
En synctest-bubbla isolerar gorutinerna som skapas inuti den.
Använd den så här:
func TestSomethingConcurrent(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
// Test concurrent code here.
})
}
Inuti bubblan:
- Goroutiner startade av testet tillhör bubblan.
- Timrar och sömnperioder använder en falsk klocka.
synctest.Waitkan vänta på att bakgrundaktivitet ska lugna ner sig.- Testet bör undvika att bero på externa goroutiner, verklig nätverks-I/O eller externa processer.
Bubblan är inte magi. Den gör inte dålig parallell design bra. Men den ger ditt test en kontrollerad miljö där tid och blockeringsbeteende är mer deterministiskt.
Problemet med time.Sleep i tester
En verklig time.Sleep i ett test betyder vanligtvis något av två saker:
Jag vet inte hur man väntar på händelsen jag faktiskt bryr mig om.
eller:
Jag vet vad jag bryr mig om, men koden som testas exponerar inget rent sätt att observera det.
Båda är designsignaler som är värda att ta på allvar — de pekar på platser där produktionskoden kan dra nytta av renare observabilitet eller mer explicita koordineringsmekanismer.
Tänk på en funktion som slutför arbete i bakgrunden:
type Worker struct {
out chan string
}
func NewWorker() *Worker {
return &Worker{
out: make(chan string, 1),
}
}
func (w *Worker) Start() {
go func() {
time.Sleep(5 * time.Second)
w.out <- "done"
}()
}
func (w *Worker) Result() <-chan string {
return w.out
}
Ett dåligt test kan se ut så här:
func TestWorkerBad(t *testing.T) {
w := NewWorker()
w.Start()
time.Sleep(6 * time.Second)
select {
case got := <-w.Result():
if got != "done" {
t.Fatalf("got %q, want done", got)
}
default:
t.Fatal("worker did not finish")
}
}
Detta test väntar sex verkliga sekunder.
Det är långsamt. Om du har många tester som detta, blir sviten smärtsam.
Ett bättre test med synctest kan avancera falsk tid omedelbart:
func TestWorkerWithSynctest(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
w := NewWorker()
w.Start()
time.Sleep(5 * time.Second)
synctest.Wait()
select {
case got := <-w.Result():
if got != "done" {
t.Fatalf("got %q, want done", got)
}
default:
t.Fatal("worker did not finish")
}
})
}
Testet uttrycker fortfarande affärsfakten — arbetaren ska slutföras efter 5 sekunder — men den spenderar inte 5 verkliga sekunder på att göra det. Det är skillnaden mellan att testa tidsberoende beteende och att slösa utvecklartid.
Testa context-timeouts
Ett av de bästa användningsområdena för testing/synctest är att testa context.Context deadlines och timeouts. Korrekt spridning av context.Canceled och context.DeadlineExceeded genom service- och handlarlager behandlas ingående i Go Error Handling Architecture: Boundaries and Patterns — synctest låter dig verifiera det beteendet utan att verklig tid går åt.
Här är en enkel funktion som väntar tills en context är avbruten:
func WaitForCancel(ctx context.Context, done chan<- error) {
go func() {
<-ctx.Done()
done <- ctx.Err()
}()
}
Utan synctest skulle testning av detta med en 30-sekunders timeout antingen göra testet långsamt eller tvinga dig att ändra timeouten bara för testets skull.
Med synctest kan du testa den verkliga timeout-varaktigheten snabbt:
func TestWaitForCancelWithTimeout(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
defer cancel()
done := make(chan error, 1)
WaitForCancel(ctx, done)
synctest.Wait()
select {
case err := <-done:
t.Fatalf("context canceled too early: %v", err)
default:
}
time.Sleep(30 * time.Second)
synctest.Wait()
select {
case err := <-done:
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("got %v, want %v", err, context.DeadlineExceeded)
}
default:
t.Fatal("context was not canceled")
}
})
}
Detta är den typ av test som synctest gör trevlig.
Du kan behålla realistiska timeout-värden i koden och fortfarande köra tester snabbt.
Testa context-avbrott
Du kan också testa explicit avbrott utan att tävla mot bakgrundsgoroutinen.
func TestWaitForCancelWithExplicitCancel(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
done := make(chan error, 1)
WaitForCancel(ctx, done)
synctest.Wait()
select {
case err := <-done:
t.Fatalf("context canceled too early: %v", err)
default:
}
cancel()
synctest.Wait()
select {
case err := <-done:
if !errors.Is(err, context.Canceled) {
t.Fatalf("got %v, want %v", err, context.Canceled)
}
default:
t.Fatal("context was not canceled")
}
})
}
Den viktiga detaljen är synctest.Wait.
Den ger bakgrundsgoroutinen en chans att observera avbrottet och lugna ner sig innan testet kontrollerar resultatet.
Vad synctest.Wait gör
synctest.Wait väntar tills alla andra goroutiner i bubblan är varaktigt blockerade.
På vanligt språk betyder det:
Vänta tills gorutinerna inuti detta test har nått en stabil blockerad punkt.
Detta är användbart när testet startar en goroutine och behöver veta att goroutinen antingen har slutförts eller väntar.
Till exempel:
func TestWaitExample(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
done := false
go func() {
done = true
}()
synctest.Wait()
if !done {
t.Fatal("goroutine did not run")
}
})
}
Detta är avsiktligt litet, men det demonstrerar idén.
synctest.Wait är inte bara en snyggare sömn — det är en synkroniseringspunkt inuti bubblan, och den distinktionen betyder mer än den först verkar.
En sömn säger:
Jag hoppas att tillräckligt med tid har passerat.
Wait säger:
Jag vill att bubblan ska nå ett stabilt blockerat tillstånd.
Det sista är mycket bättre för tester eftersom det beskriver ett observerbart tillstånd snarare än en gissa om passerad tid.
Falsk tid i en synctest-bubbla
Inuti en synctest-bubbla använder paketet time en falsk klocka.
Den falska klockan startar vid en fast tid. Den avancerar bara när varje goroutine i bubblan är varaktigt blockerad och tiden behöver gå framåt för att avblockera något.
Det betyder att detta test är snabbt:
func TestFakeTime(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
start := time.Now()
time.Sleep(1 * time.Hour)
elapsed := time.Since(start)
if elapsed != time.Hour {
t.Fatalf("got %v, want %v", elapsed, time.Hour)
}
})
}
Det ser ut som att det väntar en timme.
Det gör det inte.
Detta är användbart för att testa:
- timeouts
- deadlines
- återförsök
- backoff
- fördröjd rengöring
- hastighetsbegränsningar
- timrar
- tickers
- context-avbrott
Men det finns en viktig regel: falsk tid hjälper bara kod som använder paketet time inuti bubblan.
Om din kod beror på ett externt system, verklig nätverks-I/O eller tid mätt utanför bubblan, kan synctest inte göra det deterministiskt.
Testa en återförsöksloop
Återförsöksloopar är en vanlig källa till långsamma och flaky tester.
Här är en liten återförsökshjälp:
func Retry(ctx context.Context, attempts int, delay time.Duration, fn func() error) error {
var last error
for i := 0; i < attempts; i++ {
if err := fn(); err != nil {
last = err
} else {
return nil
}
if i == attempts-1 {
break
}
timer := time.NewTimer(delay)
select {
case <-ctx.Done():
timer.Stop()
return ctx.Err()
case <-timer.C:
}
}
return last
}
Ett normalt test kan minska förseningen till 1 millisekund bara för att hålla sviten snabb.
Det är inte så hemskt, men det betyder att testet inte längre utövar det verkliga värdet som används av produktionskoden.
Med synctest kan du behålla den verkliga förseningen:
func TestRetryEventuallySucceeds(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ctx := t.Context()
calls := 0
err := Retry(ctx, 3, 10*time.Second, func() error {
calls++
if calls < 3 {
return errors.New("temporary failure")
}
return nil
})
if err != nil {
t.Fatalf("Retry returned error: %v", err)
}
if calls != 3 {
t.Fatalf("calls = %d, want 3", calls)
}
})
}
Testet representerar två 10-sekunders väntetider.
Det körs fortfarande snabbt.
Detta är där synctest ändrar ekonomin för testning. Du behöver inte längre falska små varaktigheter spridda genom tester bara för att undvika långsam CI.
Testa återförsöksavbrott
Du kan också testa avbrott under återförsöksförsening:
func TestRetryStopsWhenContextCanceled(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
errCh := make(chan error, 1)
go func() {
errCh <- Retry(ctx, 10, 10*time.Second, func() error {
return errors.New("temporary failure")
})
}()
synctest.Wait()
cancel()
synctest.Wait()
select {
case err := <-errCh:
if !errors.Is(err, context.Canceled) {
t.Fatalf("got %v, want %v", err, context.Canceled)
}
default:
t.Fatal("Retry did not return after cancellation")
}
})
}
Detta test kontrollerar att återförsöksloopen svarar på avbrott istället för att sova genom förseningen.
Det är exakt den typ av beteende som betyder något i produktion.
Testa time.AfterFunc
time.AfterFunc är en annan bra match.
Antag att du har en funktion som schemalägger rengöring:
type Cache struct {
cleaned chan struct{}
}
func NewCache() *Cache {
return &Cache{
cleaned: make(chan struct{}, 1),
}
}
func (c *Cache) CleanupAfter(d time.Duration) {
time.AfterFunc(d, func() {
c.cleaned <- struct{}{}
})
}
func (c *Cache) Cleaned() <-chan struct{} {
return c.cleaned
}
Testet kan avancera falsk tid:
func TestCleanupAfter(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
cache := NewCache()
cache.CleanupAfter(1 * time.Minute)
synctest.Wait()
select {
case <-cache.Cleaned():
t.Fatal("cleanup happened too early")
default:
}
time.Sleep(1 * time.Minute)
synctest.Wait()
select {
case <-cache.Cleaned():
default:
t.Fatal("cleanup did not happen")
}
})
}
Detta test verifierar båda sidorna:
- Rengöringen händer inte innan förseningen.
- Rengöringen händer efter förseningen.
Och den väntar inte en verklig minut.
Testa tickers
Tickers kan också testas med falsk tid, men var försiktig. Tickers används ofta i långkörande loopar, och långkörande loopar behöver en ren avstängningsväg.
Här är en liten ticker-baserad räknare:
type Counter struct {
ticks int
done chan struct{}
}
func NewCounter() *Counter {
return &Counter{
done: make(chan struct{}),
}
}
func (c *Counter) Start(ctx context.Context, interval time.Duration) {
ticker := time.NewTicker(interval)
go func() {
defer ticker.Stop()
defer close(c.done)
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
c.ticks++
}
}
}()
}
func (c *Counter) Wait() {
<-c.done
}
func (c *Counter) Ticks() int {
return c.ticks
}
Ett test kan se ut så här:
func TestCounterTicks(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
counter := NewCounter()
counter.Start(ctx, 10*time.Second)
time.Sleep(35 * time.Second)
synctest.Wait()
cancel()
counter.Wait()
if counter.Ticks() != 3 {
t.Fatalf("ticks = %d, want 3", counter.Ticks())
}
})
}
Detta exempel har en medveten design detalj: arbetaren har en avstängningsväg.
Det är inte bara bra för tester. Det är bra för produktion.
Tester avslöjar ofta om dina goroutiner faktiskt kan stoppa.
synctest och goroutine-läckage
testing/synctest är hjälpsamt här eftersom synctest.Test väntar på att goroutiner i bubblan ska avsluta innan den returnerar, vilket betyder att läckta goroutiner är svårare att ignorera. Om en bakgrundsgoroutine aldrig avslutar, misslyckas testet istället för att tyst lämna arbete bakom sig — och det är en bra sak.
Parallell kod bör ha tydlig ägarskap. Om en funktion startar en goroutine, bör det finnas ett explicit sätt att stoppa den, eller en dokumenterad anledning till varför den får leva för evigt. I tester är “för evigt” nästan aldrig acceptabelt.
Ett bra mönster är:
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
Gör sedan så att goroutinen stoppas när contexten avbryts.
Vad “varaktigt blockerad” betyder i praktiken
De officiella dokumenten använder termen “varaktigt blockerad”.
Du behöver inte memorera varje runtime-detajl, men du bör förstå den praktiska betydelsen.
En goroutine är varaktigt blockerad när den är blockerad på ett sätt som bara kan avblockeras av något inuti samma synctest-bubbla.
Exempel inkluderar:
- ta emot från en kanal skapad inuti bubblan
- skicka till en kanal skapad inuti bubblan
- vänta på en
sync.WaitGroupassocierad med bubblan - sova med
time.Sleep - vänta på vissa timer-operationer
Vissa saker är inte varaktigt blockerade eftersom något utanför bubblan kan avblockera dem.
Exempel inkluderar:
- nätverks-I/O
- systemanrop
- externa processoperationer
- vissa mutex-väntetider
- interaktioner med goroutiner utanför bubblan
Detta är varför synctest-tester bör vara self-contained och hållas fria från extern synkronisering som bubblan inte kan se. Använd inte synctest som en wrapper runt integrationstester som pratar med det verkliga nätverket.
Vad synctest är bra för
testing/synctest är särskilt bra för enhetstester kring asynkront beteende.
Bra kandidater inkluderar:
- context-avbrott
- context-timeouts
- återförsöksloopar
- backoff-logik
- fördröjd rengöring
- timer-drivna arbetare
- ticker-drivna loopar
- bakgrundsgoroutiner
- timeout-beteende
- kanalkoordinering
time.AfterFunc- deterministiskt väntande på goroutiner
Det bästa användningsfallet är kod där den svåra delen är tid eller schemaläggning, inte extern I/O.
Vad synctest inte är bra för
testing/synctest är inte ett ersättande för all parallell testning.
Det är inte en full deterministisk schemaläggare för varje möjlig race.
Det är inte ett substitut för race-detektorn.
Det är inte ett ersättande för integrationstester.
Det gör inte verklig nätverks-I/O deterministisk.
Det fixar inte dålig goroutine-livscykel-design.
Det betyder inte att du kan ignorera kanaler, contexter, ägarskap och avstängning.
Använd synctest för rätt lager: deterministiska enhetstester för parallellt och tidsberoende beteende.
Använd andra verktyg för andra lager:
- använd
go test -raceför att upptäcka data races - använd integrationstester för verkliga beroenden
- använd lasttester för genomströmning och konkurrens
- använd benchmarkar för prestanda
- använd tracing och profiling för produktionsbeteende
synctest vs race-detektorn
testing/synctest och race-detektorn löser olika problem.
Race-detektorn hittar osäker parallell minnesåtkomst.
synctest hjälper dig att kontrollera asynkron timing och väntande i tester.
Du bör ofta använda båda.
Till exempel är detta fortfarande en race även inuti en synctest-bubbla om det inte finns någon ordentlig synkronisering:
value := 0
go func() {
value = 1
}()
_ = value
synctest.Wait kan tillhandahålla en synkroniseringspunkt för vissa testmönster, men det betyder inte att varje parallell åtkomst i din kod automatiskt är säker.
Kör parallella tester med:
go test -race ./...
Race-detektorn är fortfarande ett av de bästa verktygen Go ger dig. Att kombinera den med Go Linters: Essential Tools for Code Quality ger dig en solid statisk analys och runtime-check-baslinje för varje parallell kodbas.
synctest vs manuella falska klockor
Före testing/synctest använde många team manuella falska klockor.
Det kan fortfarande vara en bra design.
Ett manuellt klockgränssnitt kan se ut så här:
type Clock interface {
Now() time.Time
After(time.Duration) <-chan time.Time
Sleep(time.Duration)
}
Då använder produktionskod en verklig klocka och tester använder en falsk klocka.
Detta ger explicit kontroll, men det har en kostnad:
- fler gränssnitt
- mer pluggning
- fler test-abstraktioner
- fler sätt för kod att kringgå den falska klockan av misstag
synctest är attraktivt eftersom vanlig kod som använder paketet time kan köras mot falsk tid inuti testbubblan.
Det minskar behovet av klockinjektion i många fall.
Min åsikt: använd synctest när det håller produktionskoden enklare. Använd en injicerad klocka bara när klockkontroll är en del av din domändesign eller när du behöver kontroll utanför vad synctest tillhandahåller. För en bredare titt på beroendeinjektionsmönster i Go — inklusive när och hur man injicerbar abstraktioner — se Dependency Injection in Go: Patterns & Best Practices.
synctest vs kanaler och WaitGroups
Ersätt inte bra synkronisering med synctest.
Om din kod kan exponera en kompletteringskanal, en callback eller en Wait-metod, är det ofta bra design.
Till exempel:
type Server struct {
done chan struct{}
}
func (s *Server) Done() <-chan struct{} {
return s.done
}
Ett test kan vänta på det direkt.
synctest är mest användbar när beteendet som testas involverar tid, context-deadlines, bakgrundsschemaläggning eller asynkrona callbacks.
De bästa testerna kombinerar ofta båda:
- produktionskod har explicit avstängning eller kompletteringssignaler
- synctest tar bort väntetider för verklig tid
- Wait gör bakgrundaktivitet deterministisk
Vanliga misstag
Misstag 1: Att wrapa varje test i synctest
Använd inte synctest överallt. Om koden är synkron, är en vanlig testfunktion tydligare, och att lägga till bubblan wrapper introducerar bara onödig mekanik som gör tester svårare att läsa och resonera om.
Misstag 2: Att testa verklig nätverks-I/O inuti bubblan
Håll synctest-tester self-contained. Om ditt test använder en verklig nätverkssocket, extern tjänst, databas eller underprocess, tillhör det en integrationstest snarare än inuti en synctest-bubbla. Använd fakes för enhetstester och reservera verkliga beroenden för separata integrationstester där bubblaisolation inte gäller.
Misstag 3: Att läcka goroutiner
Om ditt test startar en goroutine, se till att den har en tydlig exit-väg. Använd context-avbrott, stängda kanaler eller explicita stoppmetoder — en goroutine som aldrig stoppar är både en produktionslukt och en testlukt som synctest kommer att framhålla snarare än dölja.
Misstag 4: Att bero på paket-nivå tillstånd
Paket-nivå kanaler, timrar och WaitGroups kan bryta bubblaisolation på subtila sätt. Föredra att skapa allt testtillstånd inuti synctest.Test-funktionen så att varje resurs tillhör bubblan och dess livstid är tydligt scoped till testet.
Misstag 5: Att behandla falsk tid som verklig tid
Falsk tid är för deterministiska tester, inte prestandamätning. Ett test som avancerar en timme omedelbart berättar inget användbart om CPU-kostnad, lås konkurrens, minnesanvändning eller verkligt schemaläggningbeteende i produktion — använd benchmarkar och lasttester för dessa frågor.
Misstag 6: Att ignorera race-detektorn
synctest är inte ett ersättande för go test -race, och de två verktygen löser olika problem. Kör race-detektorn alongside dina synctest-tester för att fånga osäker parallell minnesåtkomst som bubblan ensam inte kan upptäcka.
En praktisk checklista
Använd denna checklista när du skriver tester med testing/synctest.
Använd synctest när
- koden startar goroutiner
- koden använder
time.Sleep - koden använder timrar eller tickers
- koden använder context-deadlines
- koden har återförsök eller backoff-beteende
- testet för närvarande använder godtyckliga sömnperioder
- testet är flaky i CI
- testet är långsamt eftersom det väntar på verklig tid
Undvik synctest när
- koden är synkron
- testet beror på verklig nätverks-I/O
- testet beror på externa processer
- testet egentligen är en integrationstest
- du försöker mäta prestanda
- koden har ingen ren avstängningsväg
Föredrag detta mönster
func TestSomething(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
// Arrange.
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
// Act.
_ = ctx
// Let background work settle.
synctest.Wait()
// Advance fake time if needed.
time.Sleep(1 * time.Second)
synctest.Wait()
// Assert.
})
}
Detta mönster är enkelt:
- ställ upp inuti bubblan
- starta arbete inuti bubblan
- vänta på att bakgrundaktivitet ska lugna ner sig
- avancera falsk tid bara när det behövs
- assert efter synkronisering
Var att använda testing/synctest i verkliga projekt
De bästa platserna att titta är oftast inte i enkel affärslogik.
Titta efter tester med dessa lukter:
grep -R "time.Sleep" .
grep -R "time.After" .
grep -R "WithTimeout" .
grep -R "WithDeadline" .
grep -R "NewTicker" .
grep -R "AfterFunc" .
Då fråga:
- Är detta test långsamt eftersom det väntar på verklig tid?
- Är detta test flaky eftersom det utgår från att en goroutine redan har kört?
- Kan detta test isoleras från nätverk och externa processer?
- Kan bakgrundsgoroutinen stoppas rent?
- Skulle falsk tid göra assertion tydligare?
Bra kandidater lever ofta i:
- worker-paket
- återförsökspaket
- cache-paket
- scheduler-paket
- kökonsumenter
- HTTP-klientwrappers
- timeout-middleware
- bakgrundsrengöringskod
- hastighetsbegränsningskod
Börja med ett flaky test. Migrera inte hela kodbasen på en gång. Om din testsuit använder parallella tabelldrivna tester alongside asynkron kod, täcker Parallel Table-Driven Tests in Go t.Parallel()-mönstren och race-condition-fällorna som passar naturligt med synctest-approachen.
Exempel: före och efter
Här är ett realistiskt dåligt test:
func TestRetryBad(t *testing.T) {
calls := 0
err := Retry(context.Background(), 3, 500*time.Millisecond, func() error {
calls++
if calls < 3 {
return errors.New("temporary failure")
}
return nil
})
if err != nil {
t.Fatalf("Retry returned error: %v", err)
}
if calls != 3 {
t.Fatalf("calls = %d, want 3", calls)
}
}
Detta väntar cirka en sekund eftersom två återförsöksförseningar inträffar.
Det kanske inte låter dåligt, men multiplicera det med många tester och flera paket. Långsamma tester får utvecklare att köra tester mindre ofta.
Nu synctest-versionen:
func TestRetryWithSynctest(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
calls := 0
err := Retry(t.Context(), 3, 500*time.Millisecond, func() error {
calls++
if calls < 3 {
return errors.New("temporary failure")
}
return nil
})
if err != nil {
t.Fatalf("Retry returned error: %v", err)
}
if calls != 3 {
t.Fatalf("calls = %d, want 3", calls)
}
})
}
Testet behåller det verkliga förseningsvärdet, sviten förblir snabb, och avsikten är tydligare. Det är huvudvärdet av testing/synctest.
Hur man adopterar synctest säkert
Jag skulle adoptera det gradvis.
Steg 1: Hitta flaky eller långsamma parallella tester
Sök efter verkliga sömnperioder och timeout-tunga tester. Grep-kommandona i föregående avsnitt är en bra startpunkt för att identifiera kandidater över kodbasen.
Steg 2: Välj ett paket
Välj ett paket som har tydligt asynkront beteende men inte kräver verkliga externa tjänster. Worker-paket, återförsökshjälpmedel och timer-drivna komponenter är idealiska första mål.
Steg 3: Konvertera ett test
Wrap testet i synctest.Test och ersätt godtyckliga sömnperioder med synctest.Wait, falsk-tid-sömn eller explicit synkronisering. Konverteringen är vanligtvis liten — det svåraste är att se till att goroutiner har rena avstängningsvägar.
Steg 4: Kör med race-detektorn
Kör alltid med go test -race ./... efter konvertering. Ett bestått synctest-test betyder inte att koden är race-fri; det betyder bara att asynkron timing nu är deterministisk.
Steg 5: Granska goroutine-livscykel
Se till att varje goroutine startad av testet har ett sätt att exita innan bubblan stänger. Om det inte gör det, kommer synctest.Test att framhäva läckan snarare än att tyst ignorera den.
Steg 6: Upprepa bara där det förbättrar tydlighet
Konvertera inte tester bara för mode. Ett bra synctest-test bör vara mätbart snabbare, tydligare att läsa, eller mindre flaky än versionen det ersatte — om det inte är det, var konverteringen inte värd det.
Mina åsiktsstyrda regler
Använd dessa som praktiska tumregler.
Regel 1: Inga godtyckliga sömnperioder i parallella enhetstester
En sömnperiod som väntar på att en goroutine kanske ska slutföras är en lukt. Ersätt den med kanaler, WaitGroups, callbacks, synctest.Wait eller falsk tid — något som väntar på ett villkor snarare än att hoppas att tillräckligt med tid har passerat.
Regel 2: Håll synctest-tester self-contained
Skapa goroutiner, kanaler, contexter, timrar och arbetare inuti bubblan. Undvik paket-nivå delat tillstånd, vilket kan läcka mellan tester och bryta isolationen som gör synctest användbart.
Regel 3: Använd inte synctest som en integrationstest-wrapper
Om testet pratar med en verklig databas, verkligt nätverk eller extern process, håll det utanför synctest om du inte har en mycket specifik anledning att göra det.
Regel 4: Testa beteende, inte schemaläggarlycka
Målet är inte att tvinga en goroutine att köra. Målet är att verifiera observerbart beteende efter att systemet har nått ett meningsfullt tillstånd, vilket synctest.Wait gör möjligt utan att bero på timingantaganden.
Regel 5: Håll avbrottsvägar explicita
Varje bakgrundsgoroutine bör ha en avstängningsväg, och tester bör bevisa att vägen fungerar genom att avbryta contexten eller stänga kanalen och sedan verifiera att goroutinen exitar rent.
Sista tankar
testing/synctest är en av de Go-funktioner som ser liten ut men ändrar hur du skriver en klass av tester. Den ersätter inte god parallell design, race-detektorn eller behovet av integrationstester — men den gör många asynkrona enhetstester snabbare, renare och mycket mindre beroende av timinglycka.
Det betyder något eftersom parallell kod redan är tillräckligt svår. Tester bör minska osäkerhet, inte lägga till den. För en bredare vy av produktions Go-mönster över integration, kodstruktur och dataåtkomst, se App Architecture in Production.
Den praktiska slutsatsen är enkel:
Använd synctest för deterministiska enhetstester kring goroutiner, timrar, timeouts, återförsök och avbrott.
Håll verkliga sömnperioder borta från parallella tester om du inte har en mycket bra anledning.
Den vanen kommer att göra många Go-testsvider snabbare och mindre flaky.
De viktiga aktuella fakta är: testing/synctest blev allmänt tillgänglig i Go 1.25, den exponerar synctest.Test och synctest.Wait, den kör tester inuti en isolerad bubbla, och tiden inuti den bubblan använder en falsk klocka som endast avancerar när goroutiner är varaktigt blockerade.
Källor
- https://pkg.go.dev/testing/synctest
- https://go.dev/blog/testing-time
- https://go.dev/blog/synctest
- [https://go.dev/blog/testing-time](“Testing Time (and other asynchronicities) - The Go Programming Language”)
- https://go.dev/doc/go1.25
- https://go.dev/blog/go1.25