Angular Testlerinde Asenkron İşlemleri Yönetmek: waitForAsync ve fakeAsync Kullanımı
Angular uygulamaları, modern web geliştirmenin temel taşlarından biridir ve genellikle dinamik, etkileşimli kullanıcı deneyimleri sunar. Bu deneyimlerin çoğu, HTTP istekleri, zamanlayıcılar, Promise’ler ve Observable’lar gibi asenkron işlemler aracılığıyla elde edilir. Ancak, asenkron kodun test edilmesi, senkron kodun test edilmesine göre daha karmaşık ve zorlayıcıdır. Asenkron işlemlerin tamamlanmasını beklemek, testlerin kararsız olmasına (flaky tests) veya gereğinden uzun sürmesine neden olabilir.
Angular, bu zorlukların üstesinden gelmek için iki güçlü araç sunar: waitForAsync ve fakeAsync. Bu makale, her iki aracın ne olduğunu, ne zaman kullanılacağını, nasıl kullanılacağını ve Angular test stratejinizde nasıl optimize edeceğinizi derinlemesine inceleyecektir.
Asenkron İşlemler ve Test Etmenin Zorlukları
Bir Angular uygulamasında asenkron işlemlerle sıkça karşılaşırsınız:
* HTTP İstekleri: Bir API’den veri çekmek veya sunucuya veri göndermek.
* Zamanlayıcılar: setTimeout veya setInterval gibi JavaScript fonksiyonları ile belirli bir süre sonra veya düzenli aralıklarla kod çalıştırmak.
* Promise’ler: Asenkron bir işlemin başarılı veya başarısız sonucunu temsil eden nesneler.
* Observable’lar: RxJS kütüphanesinin temelini oluşturan, zamanla yayılan veri akışlarını temsil eden güçlü yapılar.
Bu işlemler, kodunuzun yürütülme sırasını etkiler ve test ortamında beklenen davranışın sağlanması için özel yaklaşımlar gerektirir. Varsayılan olarak, bir test bloğu (örneğin, it() fonksiyonu) içindeki kod senkron olarak çalışır. Eğer test, asenkron bir işlemin sonucuna bağlıysa ve bu işlem tamamlanmadan test bitiyorsa, test başarısız olur veya yanlış sonuçlar verir. İşte bu noktada waitForAsync ve fakeAsync devreye girer.
waitForAsync Kullanımı
waitForAsync, Angular’ın test yardımcılarından biridir ve Zone.js ile entegre çalışarak asenkron işlemlerin tamamlanmasını gerçekten bekler. Özellikle gerçek zamanlı asenkron operasyonları (örneğin, HTTP istekleri veya karmaşık Observable zincirleri) test ederken idealdir.
waitForAsync Nedir?
waitForAsync, bir test bloğunun içine sarmalanan asenkron işlemlerin tamamlanmasını beklemek için kullanılan bir fonksiyondur. Zone.js’in gücünden yararlanarak, test ortamında başlatılan tüm asenkron görevlerin (makro görevler ve mikro görevler dahil) bitmesini izler. Test, tüm bu görevler tamamlandığında sona erer.
Ne Zaman waitForAsync Kullanılır?
* Gerçek HTTP İstekleri: Bir servis aracılığıyla yapılan HTTP çağrılarını test ederken, özellikle HttpClientTestingModule kullanıyorsanız.
* Karmaşık Observable Zincirleri: delay, debounceTime, throttleTime gibi zaman tabanlı RxJS operatörleri içeren veya iç içe geçmiş asenkron akışlara sahip Observable’ları test ederken.
* Promise‘ler ve async/await: Promise tabanlı kodları veya async/await sözdizimi ile yazılmış fonksiyonları test ederken.
* Dış Kütüphaneler: Zone.js tarafından izlenen ve kontrol edilemeyen dış kütüphanelerin asenkron işlemlerini test ederken.
waitForAsync Nasıl Kullanılır?
waitForAsync, it() bloğunun callback fonksiyonunu sarmalar. Genellikle async/await ile birlikte kullanılarak daha okunabilir bir kod sağlar. Ayrıca, fixture.whenStable() Promise’ini kullanarak Angular’ın tüm asenkron işlemleri tamamlamasını bekleyebilirsiniz.
Aşağıdaki örnekte, bir servisten veri çeken ve bu veriyi görüntüleyen bir bileşenin nasıl test edileceğini göreceğiz.
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { delay } from 'rxjs/operators';
interface Item {
id: number;
name: string;
}
@Component({
selector: 'app-data',
template:
Veri Listesi
Yükleniyor...
- {{ item.name }}
Veri yüklenirken bir hata oluştu.
,
})
export class DataComponent implements OnInit {
items: Item[] | undefined;
loading = false;
error = false;
constructor(private http: HttpClient) {}
ngOnInit(): void {
this.loading = true;
this.http.get('/api/items')
.pipe(delay(500)) // Asenkron gecikmeyi simüle edelim
.subscribe({
next: (data) => {
this.items = data;
this.loading = false;
},
error: (err) => {
console.error('HTTP Error:', err);
this.error = true;
this.loading = false;
}
});
}
}
Şimdi bu bileşeni waitForAsync kullanarak test edelim:
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { DataComponent } from './data.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('DataComponent with waitForAsync', () => {
let component: DataComponent;
let fixture: ComponentFixture;
let httpTestingController: HttpTestingController;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [DataComponent],
schemas: [NO_ERRORS_SCHEMA] // Basitlik için diğer bileşenleri görmezden gel
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DataComponent);
component = fixture.componentInstance;
httpTestingController = TestBed.inject(HttpTestingController); // TestBed.get yerine TestBed.inject
});
afterEach(() => {
httpTestingController.verify(); // Bekleyen HTTP isteklerinin olmadığını doğrula
});
it('bileşen oluşturulmalı', () => {
expect(component).toBeTruthy();
});
it('HTTP isteği tamamlandıktan sonra veriyi yüklemeli', waitForAsync(async () => {
const mockItems: Item[] = [
{ id: 1, name: 'Öğe 1' },
{ id: 2, name: 'Öğe 2' }
];
fixture.detectChanges(); // ngOnInit'i tetikler, HTTP isteği başlar
// Yükleniyor mesajı görünmeli
expect(fixture.nativeElement.querySelector('div').textContent).toContain('Yükleniyor...');
// Bekleyen HTTP isteğini yakala ve yanıtla
const req = httpTestingController.expectOne('/api/items');
expect(req.request.method).toEqual('GET');
req.flush(mockItems); // İsteğe yanıt gönder
// fixture.whenStable() tüm asenkron görevlerin (delay dahil) tamamlanmasını bekler.
// Bu, Zone.js tarafından izlenen tüm makro ve mikro görevleri kapsar.
await fixture.whenStable();
fixture.detectChanges(); // DOM'u güncelle
// Yükleniyor mesajı kaybolmalı
expect(fixture.nativeElement.querySelector('div')).toBeNull();
// Veriler doğru şekilde yüklenmeli ve gösterilmeli
expect(component.items).toEqual(mockItems);
const listItems = fixture.nativeElement.querySelectorAll('li');
expect(listItems.length).toBe(2);
expect(listItems[0].textContent).toContain('Öğe 1');
expect(listItems[1].textContent).toContain('Öğe 2');
}));
it('HTTP isteği hata verdiğinde hata mesajını göstermeli', waitForAsync(async () => {
fixture.detectChanges(); // ngOnInit'i tetikler, HTTP isteği başlar
const req = httpTestingController.expectOne('/api/items');
expect(req.request.method).toEqual('GET');
req.error(new ErrorEvent('Network error'), { status: 500, statusText: 'Server Error' });
await fixture.whenStable(); // Hata işleyicisinin tamamlanmasını bekle
fixture.detectChanges(); // DOM'u güncelle
// Hata mesajı görünmeli
expect(fixture.nativeElement.querySelector('div').textContent).toContain('hata oluştu');
expect(component.error).toBeTrue();
}));
});
Bu örnekte:
* beforeEach(waitForAsync(...)) kullanarak test modülünün asenkron olarak yapılandırılmasını bekliyoruz.
* it('...', waitForAsync(async () => { ... })) ile test bloğunu waitForAsync içine alıyoruz. async anahtar kelimesi, await kullanmamızı sağlar.
* httpTestingController ile HTTP isteğini mock’luyor ve yanıtını kontrol ediyoruz.
* req.flush(mockItems) ile servisten gelen yanıtı simüle ediyoruz.
* await fixture.whenStable() kritik bir adımdır. Bu, Angular’ın Zone.js tarafından izlenen tüm asenkron görevlerinin (örneğin, Observable’daki delay operatörü) tamamlanmasını bekler. Bu olmadan, delay süresi bitmeden test devam edebilir ve yanlış sonuçlar verebilir.
waitForAsync Avantajları ve Dezavantajları
Avantajları:
* Gerçekçi Test Ortamı: Asenkron kodunuzu gerçek çalışma ortamına en yakın şekilde test eder.
* Güvenilirlik: Zone.js sayesinde tüm asenkron işlemlerin tamamlandığından emin olursunuz, bu da “flaky” testleri azaltır.
* Karmaşık Senaryolar: fakeAsync‘in başa çıkamadığı (örneğin, Zone.js tarafından izlenmeyen dış kütüphane asenkron işlemleri) senaryolar için uygundur.
Dezavantajları:
* Daha Yavaş Testler: Asenkron işlemlerin gerçek zamanlı olarak tamamlanmasını beklediği için testler daha uzun sürebilir.
* Hata Ayıklama: Zaman gecikmeleri nedeniyle hata ayıklama bazen daha zor olabilir.
fakeAsync Kullanımı
fakeAsync, Angular testlerinde zamanı manipüle etmenizi sağlayan güçlü bir test yardımcı aracıdır. Asenkron kodun senkron bir şekilde çalıştırılmasını sağlayarak testleri çok daha hızlı ve deterministik hale getirir.
fakeAsync Nedir?
fakeAsync, Zone.js’in bir parçası olan AsyncTestZone‘u kullanarak, test bloğu içindeki asenkron görevleri (zamanlayıcılar, Promise’ler, bazı Observable’lar) yakalar ve bunları gerçek zamanı ilerletmeden, manuel olarak kontrol etmenizi sağlar. Bu sayede, normalde milisaniyeler veya saniyeler sürecek işlemler anında tamamlanabilir.
Ne Zaman fakeAsync Kullanılır?
* Zamanlayıcılar: setTimeout, setInterval gibi JavaScript zamanlayıcılarını içeren kodları test ederken.
* Promise’ler: Promise.resolve(), Promise.reject() veya new Promise() ile oluşturulan Promise’leri test ederken.
* Basit Observable’lar: RxJS’in of(), from(), timer(), delay() gibi operatörleriyle oluşturulan ve Zone.js tarafından kontrol edilebilen Observable’ları test ederken.
* debounceTime, throttleTime gibi Zaman Tabanlı RxJS Operatörleri: Bu operatörlerin davranışını hızlıca test etmek için idealdir.
* Senkron Hale Getirilmiş Asenkron İşlemler: Gerçek HTTP istekleri yerine HttpClientTestingModule ile mock’lanmış HTTP yanıtlarını içeren senaryolarda da kullanılabilir, ancak genellikle waitForAsync daha doğal bir uyum sağlar.
Önemli Not: fakeAsync içinde gerçek bir HTTP isteği yapmaya çalışırsanız (yani HttpClientTestingModule kullanmadan), XMLHttpRequest veya fetch gibi tarayıcı API’leri Zone.js tarafından yakalanamadığı için bir hata alırsınız. Bu nedenle, fakeAsync HTTP isteklerini gerçekten yapmayan, mock’lanmış senaryolar için daha uygundur.
fakeAsync Nasıl Kullanılır?
fakeAsync, it() bloğunun callback fonksiyonunu sarmalar. İçerisinde tick() ve flush() adında iki yardımcı fonksiyon bulunur:
* tick(millis?: number): Test ortamındaki zamanı belirtilen milisaniye kadar ileri sarar. Bu, setTimeout veya setInterval gibi zamanlayıcıların tetiklenmesini sağlar. Varsayılan olarak 0 milisaniye ilerletir.
* flush(): Tüm bekleyen mikro görevleri (Promise’ler ve bazı Observable abonelikleri) anında tamamlar. tick(0) ile benzerdir ancak Promise’ler için daha açıklayıcıdır.
Aşağıdaki örneklerde, fakeAsync‘in farklı asenkron senaryolarda nasıl kullanılacağını göreceğiz.
Örnek 1: setTimeout ile Zamanlayıcı Testi
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-timer',
template:
Zamanlayıcı
{{ message }}
,
})
export class TimerComponent implements OnInit {
message: string = 'Bekleniyor...';
ngOnInit(): void {
setTimeout(() => {
this.message = 'Mesaj geldi!';
}, 1000); // 1 saniye sonra mesaj değişecek
}
}
Test kodu:
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { TimerComponent } from './timer.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('TimerComponent with fakeAsync', () => {
let component: TimerComponent;
let fixture: ComponentFixture;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TimerComponent],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
fixture = TestBed.createComponent(TimerComponent);
component = fixture.componentInstance;
});
it('1 saniye sonra mesajı değiştirmeli', fakeAsync(() => {
fixture.detectChanges(); // ngOnInit'i tetikler, setTimeout başlar
expect(component.message).toBe('Bekleniyor...');
tick(500); // 500 milisaniye ilerlet, mesaj henüz değişmemeli
fixture.detectChanges();
expect(component.message).toBe('Bekleniyor...');
tick(500); // 500 milisaniye daha ilerlet (toplam 1000ms), setTimeout tetiklenmeli
fixture.detectChanges(); // DOM'u güncelle
expect(component.message).toBe('Mesaj geldi!');
}));
it('zamanlayıcı tamamlanmadan önce bileşen yok edildiğinde mesajı değiştirmemeli', fakeAsync(() => {
fixture.detectChanges();
expect(component.message).toBe('Bekleniyor...');
tick(500); // 500 milisaniye ilerlet
fixture.destroy(); // Bileşeni yok et
tick(500); // Kalan süreyi ilerlet
// Bileşen yok edildiği için message değişmemiş olmalı (Angular'ın lifecycle yönetimi)
// Not: Gerçek bir senaryoda, setTimeout içindeki işlemin tamamlanıp tamamlanmadığını
// kontrol etmek için daha karmaşık bir mocklama gerekebilir.
// Bu örnekte, bileşen yok edildiği için setTimeout callback'i muhtemelen çalışmaz.
// Ancak, eğer callback çalışsa bile, bu testte message özelliği güncellenmeyecektir
// çünkü bileşen zaten yok edilmiştir.
expect(component.message).toBe('Bekleniyor...');
}));
});
Örnek 2: Promise Testi
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-promise',
template:
Promise Örneği
{{ data }}
,
})
export class PromiseComponent implements OnInit {
data: string = 'Yükleniyor...';
ngOnInit(): void {
this.fetchData().then((result) => {
this.data = result;
});
}
fetchData(): Promise {
return Promise.resolve('Veri Promise ile geldi!');
}
}
Test kodu:
import { ComponentFixture, TestBed, fakeAsync, flush } from '@angular/core/testing';
import { PromiseComponent } from './promise.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('PromiseComponent with fakeAsync', () => {
let component: PromiseComponent;
let fixture: ComponentFixture;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [PromiseComponent],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
fixture = TestBed.createComponent(PromiseComponent);
component = fixture.componentInstance;
});
it('Promise çözümlendiğinde veriyi göstermeli', fakeAsync(() => {
fixture.detectChanges(); // ngOnInit'i tetikler, Promise başlar
expect(component.data).toBe('Yükleniyor...');
flush(); // Tüm bekleyen Promise'leri anında tamamlar (mikro görevler)
fixture.detectChanges(); // DOM'u güncelle
expect(component.data).toBe('Veri Promise ile geldi!');
}));
});
Örnek 3: Observable (delay operatörü ile) Testi
import { Component, OnInit } from '@angular/core';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';
@Component({
selector: 'app-observable',
template:
Observable Örneği
{{ value }}
,
})
export class ObservableComponent implements OnInit {
value: string = 'Bekleniyor...';
ngOnInit(): void {
this.getDelayedValue().subscribe((val) => {
this.value = val;
});
}
getDelayedValue(): Observable {
return of('Observable verisi geldi!').pipe(delay(2000)); // 2 saniye gecikme
}
}
Test kodu:
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { ObservableComponent } from './observable.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('ObservableComponent with fakeAsync', () => {
let component: ObservableComponent;
let fixture: ComponentFixture;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ObservableComponent],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
fixture = TestBed.createComponent(ObservableComponent);
component = fixture.componentInstance;
});
it('Observable gecikme sonrası değeri güncellemelidir', fakeAsync(() => {
fixture.detectChanges(); // ngOnInit'i tetikler, Observable aboneliği başlar
expect(component.value).toBe('Bekleniyor...');
tick(1999); // 1999 milisaniye ilerlet, henüz değer gelmemiş olmalı
fixture.detectChanges();
expect(component.value).toBe('Bekleniyor...');
tick(1); // 1 milisaniye daha ilerlet (toplam 2000ms), Observable değeri yaymalı
fixture.detectChanges(); // DOM'u güncelle
expect(component.value).toBe('Observable verisi geldi!');
}));
});
fakeAsync Avantajları ve Dezavantajları
Avantajları:
* Hızlı Testler: Asenkron işlemlerin gerçek zamanlı tamamlanmasını beklemediği için testler çok hızlı çalışır.
* Deterministik: Zamanı manuel olarak kontrol ettiğiniz için test sonuçları her zaman aynıdır.
* Kolay Hata Ayıklama: Zaman gecikmeleri ortadan kalktığı için hata ayıklama daha kolaydır.
* Okunabilirlik: async/await benzeri senkron bir akış sağlar.
Dezavantajları:
* Kısıtlamalar: Zone.js tarafından yakalanamayan (örneğin, gerçek HTTP istekleri, Web Workers) asenkron işlemleri test edemez. Bu durumlarda bir hata fırlatır.
* Gerçekçilik Eksikliği: Zamanı manipüle ettiği için gerçek çalışma ortamının bazı nüanslarını yansıtmayabilir.
* tick() ve flush() Yönetimi: Karmaşık asenkron akışlarda tick() ve flush()‘ı doğru yerlerde kullanmak bazen zor olabilir.
waitForAsync ve fakeAsync Karşılaştırması ve Ne Zaman Hangisini Seçmeli?
| Özellik | waitForAsync | fakeAsync |
| :—————- | :————————————————– | :——————————————————– |
| Çalışma Prensibi | Zone.js ile gerçek asenkron görevlerin tamamlanmasını bekler. | Zone.js ile asenkron görevleri yakalar ve zamanı manuel manipüle eder. |
| Test Hızı | Daha yavaş (gerçek zaman gecikmeleri nedeniyle). | Çok hızlı (zamanı atlayarak). |
| Gerçekçilik | Yüksek (gerçek çalışma ortamına yakın). | Düşük (zaman manipülasyonu nedeniyle). |
| Kullanım Alanı | Gerçek HTTP istekleri, karmaşık RxJS akışları, dış kütüphanelerin asenkron işlemleri. | setTimeout, setInterval, Promise’ler, delay, debounceTime gibi RxJS operatörleri. |
| Hata Yakalama | Gerçek asenkron hataları yakalar. | Zone.js tarafından yakalanamayan asenkron işlemlerde hata verir. |
| Yardımcı Fonksiyonlar | fixture.whenStable(), async/await | tick(), flush() |
Genel Kural:
* fakeAsync‘i tercih edin: Mümkün olduğunca fakeAsync kullanmaya çalışın. Testlerinizin daha hızlı ve deterministik olmasını sağlar. Özellikle zamanlayıcılar, Promise’ler ve basit Observable’lar için harikadır.
* waitForAsync‘e başvurun: fakeAsync‘in kısıtlamalarına takıldığınızda veya gerçek HTTP istekleri gibi Zone.js’in kontrol edemediği dış asenkron işlemleri test etmeniz gerektiğinde waitForAsync kullanın. HttpClientTestingModule ile HTTP isteklerini mock’lasanız bile, delay gibi operatörler kullanıyorsanız waitForAsync ve fixture.whenStable() kombinasyonu daha güvenli bir seçim olabilir.
Önemli Uyarı: Aynı it bloğu içinde waitForAsync ve fakeAsync‘i birlikte kullanmaya çalışmayın. Bu, öngörülemeyen davranışlara ve hatalara yol açacaktır. Her test bloğu için tek bir asenkron test stratejisi seçmelisiniz.
En İyi Uygulamalar ve İpuçları
1. detectChanges()‘ı Unutmayın: Angular, DOM’u otomatik olarak güncellemez. Asenkron bir işlem tamamlandıktan sonra bileşenin şablonunun güncellenmesi gerekiyorsa, fixture.detectChanges()‘ı tekrar çağırmayı unutmayın.
2. TestBed.inject() Kullanın: Servisleri veya diğer bağımlılıkları testlerde almak için TestBed.get() yerine TestBed.inject() kullanın. TestBed.get() eski bir yöntemdir ve gelecekte kaldırılabilir.
3. Temizleme İşlemleri (afterEach): Özellikle mock HTTP istekleri kullanırken, httpTestingController.verify()‘yi afterEach bloğunda çağırarak bekleyen veya yanlışlıkla yapılan HTTP isteklerinin olmadığını doğrulayın. Bu, testlerinizin izole kalmasına yardımcı olur.
4. Abonelikleri Yönetin: Bileşenlerinizde Observable’lara abone oluyorsanız, hafıza sızıntılarını önlemek için ngOnDestroy yaşam döngüsü kancasında abonelikleri iptal ettiğinizden emin olun (.unsubscribe()). Testlerinizde de bu senaryoyu kontrol etmek isteyebilirsiniz.
5. Testleri Odaklayın: Her testin tek bir şeyi test etmesini sağlayın. Karmaşık asenkron akışları küçük, yönetilebilir parçalara ayırın.
6. Hata Senaryolarını Test Edin: Başarılı asenkron işlemler kadar, hata durumlarını da test edin. Örneğin, bir HTTP isteğinin başarısız olması durumunda bileşenin nasıl davrandığını kontrol edin.
7. async/await ile waitForAsync: waitForAsync ile async/await kullanmak, Promise tabanlı veya Observable tabanlı asenkron kodunuzu daha okunabilir ve yönetilebilir hale getirir. await fixture.whenStable() kilit noktadır.
Sonuç
waitForAsync ve fakeAsync, Angular geliştiricilerinin asenkron kodlarını güvenilir ve verimli bir şekilde test etmeleri için vazgeçilmez araçlardır. fakeAsync, zamanlayıcılar ve Promise’ler gibi iç asenkronlukları hızlı ve deterministik bir şekilde test etmek için mükemmeldir. waitForAsync ise gerçek HTTP istekleri ve Zone.js tarafından izlenen karmaşık asenkron akışlar için daha gerçekçi bir test ortamı sunar.
Her iki aracın da kendine özgü avantajları ve kısıtlamaları vardır. Bu nedenle, test ettiğiniz asenkron işlemin doğasına göre doğru aracı seçmek, Angular testlerinizin kalitesini, hızını ve güvenilirliğini artırmanın anahtarıdır. Bu makaledeki örnekler ve açıklamalar, Angular test stratejinizde waitForAsync ve fakeAsync‘i ustaca kullanmanız için sağlam bir temel sağlamıştır.
