Sfaturi pentru teste: evită sleep în teste
- 3 minute de citit
- 7 exemple de cod în Go
Disponibil și în English
Pe această pagină
Salut 👋,
În acest articol vreau să prezint un sfat despre teste pe care l-am învățat recent citind Software Engineering at Google: Lessons Learned from Programming Over Time. Tehnica mi-a îmbunătățit modul în care scriu teste unitare.
Când scriu teste unitare mai mari, trebuie să execut ceva în fundal, de exemplu publicarea unui mesaj către un message broker, să aștept ca mesajul să fie publicat și apoi să-l consum ca să verific că ce am publicat este corect.
Când aștept publicarea mesajului sau orice altă operație care necesită așteptare în teste, obișnuiam să apelez funcția sleep, pentru o secundă sau două. Acest lucru este acceptabil pentru câteva teste, dar dacă testele cresc, abordarea nu scalează bine. Imaginează-ți că ai 50 de teste și fiecare test doarme o secundă: ar dura cel puțin 50 de secunde să rulezi suita de teste, ceea ce înseamnă mult timp pierdut.
Abordarea mai bună este să folosești un timeout și polling: poți ** verifica la fiecare milisecundă dacă testul și-a făcut treaba, în loc să dormi **. Acest lucru va îmbunătăți testele și va reduce mult timpul de execuție!
Să demonstrez asta printr-un exemplu mic în limbajul de programare Golang. Nu voi folosi dependențe externe pentru a demonstra tehnica, dar o poți aplica oricând apelezi ceva care blochează sau dacă trebuie să aștepți ceva.
Ceea ce vom testa este un struct simplu cu o metodă care blochează și modifică un câmp.
import ( "math/rand" "time")type SystemUnderTest struct { Result string}func (s *SystemUnderTest) SetResult() { go func() { time.Sleep(time.Duration(rand.Intn(3000)) * time.Millisecond) s.Result = "the_result" }()}func (s *SystemUnderTest) GetData() string { time.Sleep(time.Duration(rand.Intn(3000)) * time.Millisecond) return "the_data"}Aceasta este ** varianta nu tocmai ideală de a o testa **:
// O metodă nu prea ideală de a testa SetResultfunc Test_SystemUnderTest_SetResult_NotIdeal(t *testing.T) { sut := SystemUnderTest{} sut.SetResult() time.Sleep(4 * time.Second) if sut.Result != "the_result" { t.Fatalf("Result not equal, want %s got %s", "the_result", sut.Result) }}SetResult durează între 0 și 3 secunde, deoarece așteptăm rezultatul, dormim 4 secunde.
=== RUN Test_SystemUnderTest_SetResult_NotIdeal--- PASS: Test_SystemUnderTest_SetResult_NotIdeal (4.00s)PASSO metodă mai bună este să scriem o buclă simplă și să facem polling pentru rezultat :
// O metodă mai bună de a testa codulfunc Test_SystemUnderTest_SetResult(t *testing.T) { sut := SystemUnderTest{} sut.SetResult() passedMilliseconds := 0 for { if passedMilliseconds > 4000 { t.Fatalf("timeout reached") } passedMilliseconds += 1 time.Sleep(1 * time.Millisecond) if sut.Result != "" { break } } if sut.Result != "the_result" { t.Fatalf("Result not equal, want %s got %s", "the_result", sut.Result) }}Scrierea unei bucle și polling-ul pentru rezultat face testul mai complex, dar se execută mai rapid. În acest caz, beneficiile depășesc dezavantajele.
=== RUN Test_SystemUnderTest_SetResult--- PASS: Test_SystemUnderTest_SetResult (2.08s)PASSDacă limbajul o permite, putem folosi și canale. Să modificăm următoarea funcție, care returnează un rezultat după un timp aleatoriu, și să o testăm.
func Test_SystemUnderTest_GetData(t *testing.T) { sut := SystemUnderTest{} timeoutTicker := time.NewTicker(5 * time.Second) result := make(chan string) // Obține rezultatul când este gata go func() { result <- sut.GetData() }() select { case <-timeoutTicker.C: t.Fatal("timeout reached") case actual := <-result: if actual != "the_data" { t.Fatalf("Data not equal, want: %s, got %s", "the_data", actual) } }}Am evitat scrierea unei bucle folosind un ticker și select.
Într-un alt caz, s-ar putea să trebuiască să testezi apeluri HTTP pe mașina locală sau cu o altă bibliotecă. Caută opțiuni de timeout.
Biblioteca HTTP din Go îți permite să specifici un timeout personalizat pentru fiecare apel pe care îl faci:
client := http.Client{ Timeout: 50 * time.Millisecond, } response, err := client.Get("http://localhost:9999/metrics") ...În concluzie
Evită folosirea lui sleep în teste. Încearcă în schimb polling pentru rezultat sau verifică dacă funcțiile care blochează au parametri sau pot fi configurate să se oprească după un interval de timeout.
Mulțumesc că ai citit și sper că ți-a plăcut acest articol! 🍻