Karma ve Jasmine'den Vitest'e Taşımak ve Angular 12'den 22'ye Testte Değişenler
Karma'dan Vitest'e geçiş iki adımda yapılır: migrate-karma-to-vitest build ayarlarını, refactor-jasmine-vitest test kodunu çevirir; ama raporun temiz olması testlerin doğru olduğunu göstermez. Bookstore'un eski istemcisinde done callback'inin kaldırılmasıyla kaybolan kontrolü, derlenmeyen mock tiplerini, fakeAsync için iki yolu, Karma ile yan yana geçişi ve Angular 12'den 22'ye testte değişenleri gerçek koşularla gösteriyoruz.
Sürüm notu: Bu yazıdaki kodlar Angular 22.2 (TypeScript 6.0), Vitest 5.1, jsdom 30, Zone.js 0.16 ve taşıma öncesi için Karma 6.4 ile Jasmine 6.3 kullanılarak Node.js 24 üzerinde derlenip denenmiştir.
Önce Tahmin Et
Bookstore'un (sitedeki serilerin ortak örnek uygulaması olan online kitapçı) eski Angular istemcisi legacy-web, Angular 22'ye yükseltilmiş ama testleri hâlâ Karma ve Jasmine'de. Testler Angular serisinin testlere giriş yazısındaki (Angular 12, Karma) yapıda yazılmış. Kitap arama service'inin testi şöyle:
it('should search books with a GET request', (done: DoneFn) => {
const books: Book[] = [{ isbn: '9780441013593', title: 'Dune', price: 450 }];
service.search('dune').subscribe((result) => {
expect(result).toEqual(books);
done();
});
const req = httpMock.expectOne('/api/books?search=dune');
expect(req.request.method).toBe('GET');
req.flush(books);
});
Angular 22'nin migration'ı ve refactor schematic'i suite'i Vitest'e taşıdı. Schematic'in raporu temiz:
| Files Scanned | 6 |
| Files Transformed | 4 |
| Files Skipped | 2 |
| Total TODOs | 0 |
## No Manual Changes Required
Test şu hâle geldi:
it('should search books with a GET request', async () => {
const books: Book[] = [{ isbn: '9780441013593', title: 'Dune', price: 450 }];
service.search('dune').subscribe((result) => {
expect(result).toEqual(books);
});
const req = httpMock.expectOne('/api/books?search=dune');
expect(req.request.method).toBe('GET');
req.flush(books);
});
Birkaç hafta sonra biri search()'e bir filter ekliyor ve service tek kitaplık sonuçları hiç yaymıyor. Taşınmış test bu hatayı yakalar mı? Eski Jasmine testi yakalar mıydı?
Cevap: done Gitti, Kontrol de Gitti
Hatayı iki sürümde de denedim. Vitest'te suite yeşil kaldı:
Tests 11 passed (11)
Karma'daki orijinal test ise kaldı:
BookService should search books with a GET request FAILED
Error: Timeout - Async function did not complete within 5000ms (set by jasmine.DEFAULT_TIMEOUT_INTERVAL)
Jasmine testinde done() iki iş yapıyordu: testi asenkron yapıyordu ve "callback en az bir kez çalıştı" kontrolü veriyordu. Callback hiç çalışmayınca done() çağrılmıyor ve test timeout'a düşüyordu. Schematic done'ı kaldırıp testi async yaptı; assertion hâlâ subscribe'ın içinde ama callback hiç çalışmazsa assertion da hiç çalışmıyor ve test boş geçiyor. Rapor "No Manual Changes Required" diyor, çünkü schematic sözdizimini doğru çevirdi; anlam ise değişti.
Doğrusu, değeri firstValueFrom ile bir Promise'e çevirip await etmek. Observable değer yaymadan tamamlanırsa Promise reddediliyor:
it('should search books with a GET request', async () => {
const books: Book[] = [{ isbn: '9780441013593', title: 'Dune', price: 450 }];
const result = firstValueFrom(service.search('dune'));
const req = httpMock.expectOne('/api/books?search=dune');
expect(req.request.method).toBe('GET');
req.flush(books);
expect(await result).toEqual(books);
});
Aynı hatayla bu test kaldı: EmptyError: no elements in sequence. Bu yazı Karma ve Jasmine'den Vitest'e taşımanın adımlarını, schematic'in neyi çevirip neyi çeviremediğini ve Angular 12'den 22'ye testte nelerin değiştiğini anlatıyor. Vitest'in Angular 21'de varsayılan olmasını Angular serisinin Angular 21 yazısı anlatıyor; burada var olan bir suite'i taşıyoruz.
Taşımanın İki Adımı
Taşıma iki ayrı araçla yapılıyor: biri build ayarlarını, diğeri test kodunu değiştiriyor.
Birinci adım angular.json. legacy-web'in test target'ı eski @angular/build:karma builder'ını kullanıyordu ve polyfill'ler, asset'ler, stiller test target'ının içindeydi. Angular 22'de bunu çeviren migration ng update'in isteğe bağlı migration'larından biri:
npx ng update @angular/cli --name migrate-karma-to-vitest
--- Karma to Vitest Migration Summary ---
Projects migrated: 1
- legacy-web
Note: To refactor your test files from Jasmine to Vitest, consider running the following command:
ng g @schematics/angular:refactor-jasmine-vitest <project_name>
UPDATE tsconfig.spec.json (409 bytes)
UPDATE angular.json (2560 bytes)
UPDATE package.json (1061 bytes)
Migration test target'ını @angular/build:unit-test'e çevirdi ve runner: "vitest" yazdı. Test'e özgü build seçenekleri yeni bir testing build configuration'ına taşındı; Karma'daki davranışı korumak için aot: false ve zone.js/testing polyfill'i de orada. tsconfig.spec.json'da jasmine tipleri vitest/globals oldu ve package.json'a vitest eklendi. Eklenmeyen bir şey vardı; ilk koşu şöyle bitti:
The following packages are required but were not found:
- A DOM environment is required for non-browser tests. Please install either "jsdom" or "happy-dom".
npm install --save-dev jsdom sonrasında build bu kez test dosyalarında durdu: TS2304: Cannot find name 'jasmine', Cannot find name 'DoneFn', Cannot find name 'spyOn'. Test kodu hâlâ Jasmine.
İkinci adım test kodunu çeviren schematic. --fake-async seçeneği, fakeAsync testlerini de Vitest'in fake timer'larına çeviriyor:
npx ng g @schematics/angular:refactor-jasmine-vitest --fake-async
Çevirilerin çoğu mekanik ve doğruydu. jasmine.createSpyObj yerine vi.fn() taşıyan bir nesne geldi, and.returnValue mockReturnValue oldu, fail() expect.fail() oldu. Jasmine'de spyOn varsayılan olarak gerçek method'u çağırmaz, Vitest'te çağırır; schematic bu farkı biliyor, and.callThrough()'u gereksiz diye kaldırdı. Debounce'lu arama testinin çevrilmiş hâli:
describe('BookSearchComponent', () => {
beforeEach(() => {
vi.useFakeTimers({ advanceTimeDelta: 1, shouldAdvanceTime: true });
});
afterEach(() => {
vi.useRealTimers();
});
it('should search after the user stops typing', async () => {
const input = fixture.nativeElement.querySelector('input') as HTMLInputElement;
input.value = 'dune';
input.dispatchEvent(new Event('input'));
await vi.advanceTimersByTimeAsync(299);
expect(bookService.search).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(1);
fixture.detectChanges();
expect(bookService.search).toHaveBeenCalledWith('dune');
expect(fixture.nativeElement.querySelector('li').textContent).toBe('Dune');
});
});
tick(299) → await vi.advanceTimersByTimeAsync(299) çevirisi 2. yazıda fake timer'larla yaptığımızın aynısı. Schematic beforeEach'i describe'ın en üstüne ekliyor; okunabilirlik için let tanımlarının altına taşımak isteyebilirsin.
Rapor Temiz, Build Kırmızı
Schematic raporu sıfır TODO (elle yapılması gereken iş notu) dediği hâlde çevrilmiş kod derlenmedi:
✘ [ERROR] TS2322: Type '{ getTotal: Mock<Procedure>; }' is not assignable to type 'MockedObject<CartService>'.
Type '{ getTotal: Mock<Procedure>; }' is missing the following properties from type 'CartService': items, add
createSpyObj'in yerine gelen nesne yalnız getTotal'ı taşıyor, ama değişkenin tipi bütün CartService'i bekleyen MockedObject<CartService>. Jasmine'in SpyObj tipi bu konuda daha esnekti. İki dosyada elle düzelttik; tipi, mock'un gerçekten taşıdığı method'la sınırladık:
let cartSpy: { getTotal: Mock<CartService['getTotal']> };
beforeEach(() => {
cartSpy = {
getTotal: vi.fn<CartService['getTotal']>().mockName('CartService.getTotal'),
};
TestBed.configureTestingModule({
providers: [{ provide: CartService, useValue: cartSpy }],
});
service = TestBed.inject(ShippingService);
});
HttpClientTestingModule kullanan testi de elle güncelledik; Angular 18'den beri deprecated, yerine providers: [provideHttpClient(), provideHttpClientTesting()]. Bu değişiklik test runner'la ilgili olmadığı için schematic'in kapsamında değil. Son olarak Karma paketlerini kaldırdık (karma, karma-chrome-launcher, karma-coverage, karma-jasmine, karma-jasmine-html-reporter, jasmine-core, @types/jasmine; Karma'da coverage için gereken istanbul-lib-instrument de). 6 dosya ve 11 test Vitest'te 1,0–1,1 saniyede geçti. Bu boyuttaki bir suite'te süre kazancı yok: Karma'nın tarayıcıyı açması dahil komutun toplam süresi 2,1 saniye, Vitest'inki 1,9–2,1 saniye. Kazanç başka yerde: CI'da tarayıcı kurmaya gerek yok, vi API'si ve Vitest'in watch modu, 1.–8. yazılardaki araçların tamamı.
fakeAsync İçin İki Yol
fakeAsync ve tick, Zone.js'in Jasmine için yamalanmış hâline dayanıyor. Schematic'i --fake-async olmadan çalıştırınca fakeAsync testleri olduğu gibi kaldı ve Vitest'te ikisi de kaldı:
Error: Expected to be running in 'ProxyZone', but it was not found.
İki çözüm var. Birincisi yukarıdaki gibi --fake-async ile testleri Vitest fake timer'larına çevirmek. İkincisi, Angular dokümanında anlatıldığı gibi, Zone.js'in Vitest yamasını test polyfill'lerine eklemek:
"testing": {
"aot": false,
"optimization": false,
"extractLicenses": false,
"polyfills": ["zone.js", "zone.js/testing", "zone.js/plugins/vitest-patch"]
}
Bu satırla fakeAsync testleri değişmeden geçti. Angular ekibi native async ve Vitest fake timer'larını öneriyor; uygulama zoneless'a geçtiğinde fakeAsync zaten çalışmayacak. Bookstore'da yamayı yalnız geçiş süresince, çok sayıda fakeAsync testi olan bir suite'i önce yeşile çekmek için kullanırız.
Kademeli Geçiş: Karma'da Kalmak ve Yan Yana Çalıştırmak
Büyük bir suite tek seferde taşınmak zorunda değil. Unit-test builder'ı Karma'yı hâlâ destekliyor: runner: "karma" yazınca aynı builder testleri Karma ile çalıştırıyor; ng new --test-runner=karma de bunu üretiyor. Geçiş süresince taşınmamış dosyaları ayrı bir target'ta Karma ile, taşınanları Vitest ile çalıştırmak mümkün. Denediğim yapı:
"test-karma": {
"builder": "@angular/build:unit-test",
"options": {
"runner": "karma",
"tsConfig": "tsconfig.spec.karma.json",
"buildTarget": ":build:testing",
"include": ["src/app/cart/cart.service.spec.ts"],
"browsers": ["ChromeHeadless"]
}
}
tsconfig.spec.karma.json jasmine tiplerini, tsconfig.spec.json vitest/globals'ı kullanıyor; iki tip seti aynı tsconfig'te çakışıyor. ng run legacy-web:test-karma iki Jasmine testini Karma'da, ng test taşınanları Vitest'te çalıştırdı. Dosya dosya taşıyıp include listesini küçülttükçe Karma target'ı boşalıyor; boşalınca target'ı ve Karma paketlerini sil. Bu dönemi kısa tut: iki runner, iki tip seti ve iki farklı matcher davranışı ekibin kafasını karıştırıyor.
Angular 12'den 22'ye Testte Değişenler
Eski bir test dosyasını açtığında hangi satırın hangi sürümde eskidiğini bilmek, neyi değiştireceğine karar vermeyi kolaylaştırıyor. Sürümleri Angular'ın changelog'undan ve blog yazılarından doğruladık:
| Sürüm | Değişiklik | Eski testte görünüşü |
|---|---|---|
| 12 | Protractor yeni projelere eklenmiyor ve deprecated; Angular blog'una göre Eylül 2023'te End of Life | e2e/protractor.conf.js, browser.get, element(by.css(...)) |
| 15.2 | RouterTestingHarness | RouterTestingModule ve router.navigate + fixture.detectChanges |
| 18 | HttpClientTestingModule deprecated, yerine provideHttpClientTesting() | imports: [HttpClientTestingModule] |
| 19 | component'ler varsayılan olarak standalone | declarations: [XComponent] |
| 20 | TestBed.flushEffects() kaldırıldı, yerine TestBed.tick(); 20.2'de zoneless stable | flushEffects() çağrıları |
| 21 | Vitest varsayılan test runner, zoneless varsayılan; RouterTestingModule için migration | karma.conf.js, src/test.ts, fakeAsync |
| 22 | OnPush varsayılan, TestBed.getLastFixture(), migrate-karma-to-vitest ve refactor-jasmine-vitest --fake-async | componentInstance üzerinden yazılan state |
Protractor ile yazılmış uçtan uca (E2E) testleri Playwright'a taşımayı Integration ve E2E Test serisinin eski testleri okuma yazısı anlatıyor. 19'daki değişikliğin etkisi, eski bir component'in standalone: false satırını silince görüldü: declarations kullanan üç test "is marked as standalone and can't be declared in any NgModule - did you intend to import it instead" hatasıyla kaldı. Standalone bir component testte imports'a yazılıyor. Zoneless ve OnPush'un eski testlerdeki etkisini, componentInstance.count = 2 gibi satırların neden artık ekranı güncellemediğini 3. yazıda gördük.
Üretimde Dikkat
Önce build migration'ını, sonra kod schematic'ini ayrı commit'lerde çalıştır. Diff'ler küçük kalıyor ve bir sorun çıktığında hangi adımdan geldiği belli oluyor. Schematic'in raporunu (jasmine-vitest-<tarih>.md) pull request'e ekle; ama "No Manual Changes Required" satırını testlerin doğru olduğunun kanıtı sayma.
Taşımadan önce ve sonra test sayısını karşılaştır. Karma 11 test çalıştırdı, Vitest de 11. Sayı düşerse bir dosya include dışında kalmış ya da testler sessizce atlanmış demektir. Coverage de aynı şekilde: Vitest'te coverage @vitest/coverage-v8 ile geliyor (1. yazı); Karma'nın istanbul tabanlı raporuyla birebir aynı sayıları bekleme, ölçüm yöntemleri farklı.
Asenkron testleri tek tek gözden geçir. done, fakeAsync ve waitForAsync kullanan testler taşımanın en riskli kısmı. git grep "subscribe(" ile spec dosyalarında callback içinde assertion yapan testleri bulup firstValueFrom ya da lastValueFrom'a çevirmek bir öğleden sonranın işi.
Sık Düşülen Tuzaklar
Çevrilemeyen testleri sessizce skip etmek. Schematic bir testi çeviremediğinde TODO yorumu bırakıyor; ekip suite'i yeşile çekmek için o testleri it.skip ile kapatabiliyor. Vitest bunu yalnız özet satırında gösteriyor:
Test Files 6 passed (6)
Tests 10 passed | 1 skipped (11)
"6 passed" yazan bir satırın altındaki "1 skipped" gözden kaçıyor. Atlanan testleri bir issue'ya bağla ve CI'da atlanan test sayısını izle; yapılacak bir test için it.todo daha dürüst bir işaret.
done'ın taşıdığı kontrolü kaybetmek. Yazının başındaki durum: callback içindeki assertion çalışmasa da test geçiyor. firstValueFrom ya da expect.assertions(n) ile kontrolü geri getir.
jsdom'u kurmayı unutmak. Migration vitest'i ekliyor ama DOM (tarayıcının sayfayı tuttuğu element ağacı) ortamını eklemiyor; hata mesajı açık, ama CI'da ilk kez görünce şaşırtıyor.
Migration ile yeni API'leri aynı anda uygulamak. Runner'ı değiştirirken bir yandan HttpClientTestingModule'ü, declarations'ı ve fakeAsync'i de değiştirince kırmızı bir testin sebebini bulmak zorlaşıyor. Önce aynı testleri Vitest'te yeşile çek, sonra modernleştir.
Küçük Bir Deneme
★☆☆ legacy-web'deki BookService testine, filter eklendiğinde kalacak şekilde yazılmış bir test daha ekle: boş liste dönen bir arama için firstValueFrom ne yapıyor?
★★☆ WishlistButtonComponent'i standalone yap, CatalogModule'ü kaldır ve testleri imports ile güncelle. Ardından testi 4. yazıdaki gibi Angular Testing Library ile yeniden yaz.
★★★ BookSearchComponent'i signal'lara ve 2. yazıdaki debounced()'a taşı, Zone.js'i kaldırıp uygulamayı zoneless yap. Hangi testler kaldı ve fixture.detectChanges() satırlarının yerine ne geldi?
Bu yazıyla Frontend Testleri serisi bitti. Dokuz yazıda Bookstore'un Angular istemcisini önce jsdom'da saf fonksiyon, service, component ve routed feature seviyesinde, sonra harness'larla, gerçek tarayıcıda ve Playwright ile uçtan uca test ettik; son olarak eski bir suite'i bugünün araçlarına taşıdık. Buradan sonra testleri CI'da çalıştırmak için GitHub Actions serisinin "GitHub Actions ile Angular CI" yazısına, gerçek backend'le full-stack akışlar için Integration ve E2E Test serisine geçebilirsin.