Angular 22'de Test Altyapısı: Vitest, unit-test Builder'ı, jsdom ve Neyi Hangi Seviyede Test Etmeli?
Angular 22'nin unit-test builder'ı Vitest'i isolation kapalı çalıştırır; bu yüzden module'de tutulan state, worker sayısına göre test dosyaları arasında taşınabilir ve lokalde yeşil olan suite CI'da kırmızıya döner. Bu yazıda Bookstore'un Angular istemcisinde test seviyelerini ve test haritasını çıkarıyor, angular.json test target'ını, setupFiles, providersFile ve runnerConfig'i kuruyor, jsdom ile happy-dom'u ölçüyor ve testleri tek tek çalıştırıp debug ediyoruz.
Sürüm notu: Bu yazıdaki kodlar Angular 22.2 (TypeScript 6.0), Vitest 5.1 ve jsdom 30 ile Node.js 24 üzerinde derlenip denenmiştir.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) Angular istemcisi bookstore-web'de kitap detay sayfası, kullanıcının son baktığı üç kitabı hatırlıyor. İlk sürüm, state'i bir module'ün içindeki array'de tutuyordu:
// src/app/catalog/recently-viewed.ts
const limit = 3;
const recent: string[] = [];
export function markViewed(isbn: string): void {
const index = recent.indexOf(isbn);
if (index >= 0) {
recent.splice(index, 1);
}
recent.unshift(isbn);
recent.length = Math.min(recent.length, limit);
}
export function recentlyViewed(): readonly string[] {
return recent;
}
Testler iki dosyaya bölünmüştü; biri sıralamayı, diğeri üç kitap sınırını kontrol ediyor:
// recently-viewed.spec.ts
it('puts the latest book first', () => {
markViewed('9780441013593'); // Dune
markViewed('9780141439587'); // Emma
expect(recentlyViewed()).toEqual(['9780141439587', '9780441013593']);
});
// recently-viewed-limit.spec.ts
it('keeps only the last three books', () => {
['9780261103344', '9780141439549', '9780141439587', '9780441013593'].forEach(markViewed);
expect(recentlyViewed()).toHaveLength(3);
expect(recentlyViewed()[0]).toBe('9780441013593');
});
Geliştiricinin makinesinde ng test dört dosyada on testin hepsini geçiriyor. Aynı commit, 2 vCPU'lu bir CI runner'ında kırmızı. Hangi test kaldı ve neden lokalde geçti? İpucu: iki dosya Vitest'te ayrı ayrı mı çalışıyor, yoksa bir şeyi paylaşıyor mu?
Cevap: Test Dosyaları Aynı Module'ü Paylaşabiliyor
CI'daki çıktı şuydu:
❯ bookstore-web src/app/catalog/recently-viewed.spec.ts (1 test | 1 failed) 2ms
❯ recently viewed books (1)
× puts the latest book first 2ms
FAIL bookstore-web src/app/catalog/recently-viewed.spec.ts > recently viewed books > puts the latest book first
AssertionError: expected [ '9780141439587', …(2) ] to deeply equal [ '9780141439587', '9780441013593' ]
- Expected
+ Received
[
"9780141439587",
"9780441013593",
+ "9780141439549",
]
Test Files 1 failed | 3 passed (4)
Tests 1 failed | 9 passed (10)
Listenin sonundaki Middlemarch, öbür dosyadaki sınır testinden kalma. Bunun iki sebebi var. Birincisi, Angular'ın @angular/build:unit-test builder'ı Vitest'i isolate: false ile çalıştırıyor. Builder'ın schema'sı bunu Karma/Jasmine deneyimine uyum için yapılmış bir varsayılan olarak açıklıyor; Karma da bütün spec'leri tek bir tarayıcı sayfasında koştururdu. Isolation kapalıyken aynı worker'a düşen test dosyaları module'leri yeniden yüklemiyor, recent array'i dosyadan dosyaya taşınıyor. İkincisi, Vitest tek seferlik koşuda worker sayısını çekirdek sayısının bir eksiği olarak hesaplıyor. On çekirdekli geliştirici makinesinde dokuz worker'a izin vardı; dört dosyanın her biri kendi worker'ını aldı ve hiçbir şey paylaşılmadı. İki çekirdekli runner'da tek worker kaldı ve dört dosya aynı process'te sırayla çalıştı. CI ortamını lokalde taklit etmek için vitest-base.config.mts dosyasına geçici olarak maxWorkers: 1 yazdım; çıktı yukarıdaki.
Sorun yalnız küçük makinelere özgü değil. Aynı spec'ten yirmi kopya ekleyip suite'i on çekirdekte iki kez çalıştırdığımda bir koşuda 14, diğerinde 13 dosya kaldı. Dosya sayısı worker sayısını geçince dosyalar worker'ları paylaşıyor ve hangi dosyanın hangisinden sonra çalışacağı her koşuda değişiyor. Kırmızının sayısı koşudan koşuya değişen bir suite, flaky testin tanımı.
ng test --isolate her dosyaya temiz bir ortam veriyor ve 23 dosyalık deneme suite'i her seferinde geçti. Bedeli süre: üç koşunun ortalamasında isolation'sız 0,7 saniye, isolation'lı 1,75 saniye. Asıl çözüm, test dosyaları arasında yaşayan state'i ortadan kaldırmak. Angular'da uygulama boyunca yaşayan state'in yeri root injector'daki bir service; TestBed her testten sonra injector'ı sıfırladığı için state testler arasında taşınmıyor:
import { Injectable, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class RecentlyViewed {
private readonly isbns = signal<readonly string[]>([]);
readonly list = this.isbns.asReadonly();
mark(isbn: string): void {
this.isbns.update((list) => [isbn, ...list.filter((item) => item !== isbn)].slice(0, 3));
}
}
import { TestBed } from '@angular/core/testing';
import { RecentlyViewed } from './recently-viewed';
describe('RecentlyViewed', () => {
it('puts the latest book first', () => {
const recent = TestBed.inject(RecentlyViewed);
recent.mark('9780441013593'); // Dune
recent.mark('9780141439587'); // Emma
expect(recent.list()).toEqual(['9780141439587', '9780441013593']);
});
it('keeps only the last three books', () => {
const recent = TestBed.inject(RecentlyViewed);
['9780261103344', '9780141439549', '9780141439587', '9780441013593'].forEach((isbn) => recent.mark(isbn));
expect(recent.list()).toEqual(['9780441013593', '9780141439587', '9780141439549']);
});
});
Bu hâliyle maxWorkers: 1 ile üç kez çalıştırdım, üçünde de geçti. TestBed.inject ve service testlerinin geri kalanı bir sonraki yazının konusu. Bu yazıda önce testlerin nerede, nasıl ve hangi ayarlarla çalıştığını oturtuyoruz.
Angular Uygulamasında Test Seviyeleri
Test türlerini, pyramid ile trophy arasındaki farkı ve bir davranışı hangi seviyede test edeceğine nasıl karar vereceğini Test Stratejisi serisinin "Test Pyramid, Testing Trophy ve Honeycomb" ve "Neyi Hangi Seviyede Test Etmeli?" yazıları anlatıyor. Burada o kavramların Angular'daki karşılığına bakıyoruz. Bir Angular uygulamasında beş seviye var ve her biri farklı bir aracı kullanıyor:

- Saf fonksiyon: Angular'a hiç dokunmayan kod. ISBN (kitaplara verilen uluslararası standart numara) doğrulama, sepet toplamı, fiyat yuvarlama. TestBed gerekmez, bu yazıdaki
isbn.spec.tstestleri 0–1 ms sürdü. - Service:
inject()kullanan, signal ya dahttpResourcetaşıyan class'lar. TestBed injector'ı kurar, HTTPHttpTestingControllerile taklit edilir. 2. yazının konusu. - Component: template, input, output ve DOM (tarayıcının sayfayı tuttuğu element ağacı).
TestBed.createComponentya da Testing Library ile, jsdom içinde. Bu yazıdakiApptesti 19 ms sürdü. 3. ve 4. yazıların konusu. - Routed feature: bir route'un arkasındaki sayfa, gerçek child component'leri, guard'ı ve resolver'ıyla birlikte; yalnız HTTP taklit edilir.
RouterTestingHarnessve MSW (Mock Service Worker; HTTP isteklerini network katmanında yakalayıp sahte cevap döndüren library) ile, 5. yazıda. - E2E (uçtan uca): derlenmiş uygulama gerçek bir tarayıcıda, Playwright ile. API'yi
page.routeile taklit edip yalnız UI akışını test ediyoruz (8. yazı). Gerçek API ve veritabanıyla full-stack E2E, Integration ve E2E Test serisinin konusu.
Bookstore için test haritası şöyle:
| Davranış | Seviye | Neden o seviye |
|---|---|---|
| ISBN-13 check digit'i | saf fonksiyon | hesaplama, DOM ve dependency injection yok |
| Kategori filtresi değişince doğru URL'e istek gitmesi | service | HTTP isteğinin şekli service'in sözleşmesi |
| Kitap kartında stok bitince "Add to cart" düğmesinin kapanması | component | davranış template'te |
| Checkout formunda hata mesajlarının görünmesi | component (Testing Library) | kullanıcının gördüğü metin ve erişilebilir isimler |
| Sipariş detay sayfasının başkasının siparişini açmaması | routed feature | guard, route param ve HTTP birlikte çalışıyor |
| Kitap ara, sepete ekle, checkout | E2E | birden çok sayfa ve gerçek tarayıcı davranışı |
Karar kuralı kısa: davranışı görebilen en alt seviyede test et. Fiyat yuvarlama hatası component testinde de yakalanır ama kaldığında sebebi template'te mi hesaplamada mı, ayıramazsın. Tersine, guard'ın route param'ı yanlış okuması saf bir unit testte hiç görünmez; o hata yalnız parçalar birleşince ortaya çıkıyor. Trophy'nin ağırlık verdiği integration katmanı Angular'da çoğu zaman component ve routed feature testleri oluyor: gerçek template, gerçek child component'ler, sınırda taklit edilmiş HTTP.
ng new Ne Kuruyor?
npx @angular/cli@22.2.7 new bookstore-web ile açılan projede test için üç şey var. package.json'da vitest (^5.0.0) ve jsdom (^30.0.0) devDependency olarak geliyor; typescript ise ~6.0.2. Angular 22.2'nin peer dependency aralığı >=6.0 <6.1, bu yüzden Angular projelerinde TypeScript 6.0 kullanıyoruz; npm'deki TypeScript 7.0 bu projeye girmiyor. angular.json'daki test target'ı tek satır:
"test": {
"builder": "@angular/build:unit-test"
}
tsconfig.spec.json ise vitest/globals tiplerini ekliyor. Bu sayede spec dosyalarında describe, it, expect ve vi import edilmeden kullanılıyor; builder Vitest'i globals: true ile çalıştırıyor. Import'ları açıkça yazmayı tercih edersen import { describe, expect, it } from 'vitest'; da çalışıyor.
Ayarı olmayan bir builder'ın varsayılanlarını builder'ın kendi schema'sından okumak en güvenilir yol (node_modules/@angular/build/src/builders/unit-test/schema.json). include varsayılanı **/*.spec.ts ve **/*.test.ts. Ortam, happy-dom kuruluysa happy-dom, değilse jsdom. isolate kapalı. watch terminalde açık, terminal dışında kapalı. quiet lokalde açık, CI'da kapalı; bu yüzden CI log'unda her koşuda bir chunk tablosu görürsün.
angular.json'daki test Target'ını Açmak
Bookstore'un test target'ı yazının sonunda şu hâlde:
"test": {
"builder": "@angular/build:unit-test",
"options": {
"setupFiles": ["src/test-setup.ts"],
"providersFile": "src/test-providers.ts",
"runnerConfig": "vitest-base.config.mts"
}
}
setupFiles her test dosyasından önce çalışan dosyalar. Builder'ın açıklamasına göre uygulamanın polyfill'leri ve TestBed bu dosyalardan önce hazırlanıyor. Bookstore'da Türkçe locale verisini burada kaydediyoruz; uygulamada aynı satır main.ts'te duruyor:
// src/test-setup.ts
import { registerLocaleData } from '@angular/common';
import localeTr from '@angular/common/locales/tr';
registerLocaleData(localeTr);
providersFile, default export'u bir provider array'i olan dosya. Bu provider'lar her testte TestBed'e ekleniyor. Uygulamanın locale provider'larını src/app/locale.ts'e taşıyıp hem app.config.ts'te hem burada kullanıyoruz; böylece testler uygulamayla aynı LOCALE_ID ile koşuyor. 2. yazıdaki PricePipe testleri buna dayanacak:
// src/app/locale.ts
import { DEFAULT_CURRENCY_CODE, LOCALE_ID, Provider } from '@angular/core';
export const localeProviders: Provider[] = [
{ provide: LOCALE_ID, useValue: 'tr' },
{ provide: DEFAULT_CURRENCY_CODE, useValue: 'TRY' },
];
// src/test-providers.ts
import { localeProviders } from './app/locale';
export default localeProviders;
Angular dokümanı, yeni eklenen setup dosyalarının tsconfig.spec.json'ın include listesine de yazılmasını söylüyor. Yazmasan da testler çalıştı; ama editör ve tsc -p tsconfig.spec.json bu dosyaları tip kontrolünden geçirmiyor.
runnerConfig, Vitest'in kendi config dosyası. ng generate config vitest boş bir vitest-base.config.mts üretip angular.json'a "runnerConfig": true ekliyor; true olduğunda builder dosyayı kendisi arıyor ve her koşunun başına "Automatically searching for and using Vitest configuration file." yazıyor. Dosya yolunu açıkça verdim. Bookstore'da içinde tek ayar var:
// vitest-base.config.mts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
restoreMocks: true,
},
});
restoreMocks, her testten önce vi.restoreAllMocks() çağırıyor ve vi.spyOn ile değiştirilmiş method'ların orijinal hâlini geri getiriyor. Ayarın builder'dan geçtiğini iki testlik bir denemeyle kontrol ettim: ilk testte vi.spyOn(clock, 'now').mockReturnValue(42), ikinci testte clock.now() ayar açıkken orijinal değeri, kapalıyken 42 döndürdü. Spy'lar 5. yazıda yoğun olarak kullanılacak.
Dosya uzantısı önemli. Config'i ilk denemede vitest-base.config.ts adıyla yazdığımda Vite, CommonJS olarak yüklenen bir dosyada ESM (import/export ile çalışan standart JavaScript module sistemi) syntax'ı kullanıldığını söyleyen bir uyarı bastı ve .mjs uzantısını ya da "type": "module" ayarını önerdi. .mts uzantısı bu uyarıyı ortadan kaldırıyor.
Config dosyası her Vitest ayarını değiştirmene izin vermiyor. Builder test dosyalarını kendisi buluyor; config'e include yazınca şu uyarıyı basıp ayarı siliyor:
[vite] warning: The "test.include" option in the Vitest configuration file is not supported. The Angular CLI Test system will manage test file discovery.
Builder'ın kaynak koduna göre projects için de aynı şey geçerli; watch ise CLI'ın --watch seçeneğiyle eziliyor. Bu ayarların yeri angular.json ya da komut satırı.
Coverage için @vitest/coverage-v8 paketini kurmak gerekiyor. Kurulu değilken ng test --coverage MISSING DEPENDENCY Cannot find dependency '@vitest/coverage-v8' hatasıyla duruyor. Paketi npm i -D @vitest/coverage-v8@5.1.0 ile ekledikten sonra:
$ npx ng test --no-watch --coverage --coverage-reporters text
=============================== Coverage summary ===============================
Statements : 100% ( 34/34 )
Branches : 100% ( 19/19 )
Functions : 100% ( 7/7 )
Lines : 100% ( 21/21 )
================================================================================
Bu %100'ün neyi söylemediğini Test Stratejisi serisinin "Code Coverage Ne Söyler, Ne Söylemez" yazısı anlatıyor; burada yalnız kurulumu yapıyoruz. browsers seçeneği testleri gerçek tarayıcıda çalıştıran Vitest browser mode'u açıyor, ona 7. yazıda geleceğiz.
jsdom mu, happy-dom mu?
Testler Node.js içinde çalışıyor; DOM'u jsdom ya da happy-dom taklit ediyor. İkisinin farkını görmek için iki ortamda aynı küçük testi çalıştırdım. Değerleri hata mesajında okuyabilmek için test bilerek kalıyor:
it('probes the DOM environment', () => {
const div = document.createElement('div');
div.innerHTML = '<p>Dune</p><p style="display:none">Hidden</p>';
document.body.appendChild(div);
const probe = [
div.innerText,
div.textContent,
div.getBoundingClientRect().height,
typeof globalThis.IntersectionObserver,
typeof window.matchMedia,
];
throw new Error(JSON.stringify(probe));
});
jsdom 30: Error: [null,"DuneHidden",0,"undefined","undefined"]
happy-dom 21: Error: ["Dune","DuneHidden",0,"function","function"]
jsdom'da innerText tanımlı değil (undefined, JSON'da null görünüyor), IntersectionObserver ve matchMedia da yok. happy-dom'da innerText "Dune" döndü, yani gizli paragrafı atladı; iki API de var. textContent ikisinde de "DuneHidden", element'in yüksekliği ikisinde de 0: hiçbiri layout hesaplamıyor. Süre farkı da var. Bookstore'un üç dosyalık suite'i üç koşuda jsdom ile 383–389 ms, happy-dom ile 261–264 ms sürdü. Farkın büyük kısmı ortamın kurulma süresi; Vitest'in özet satırına göre jsdom'da sürenin %62'si, happy-dom'da %44'ü ortam kurulumuna gitti.
Bookstore'da jsdom'da kalıyoruz. Gerekçe şu: happy-dom'un fazladan sağladığı API'ler gerçek davranışı değil, bir taklidi veriyor. IntersectionObserver happy-dom'da var ama element'in görünür olup olmadığını gerçek bir layout'a göre hesaplamıyor. Sonsuz kaydırma gibi layout'a bağlı bir davranışı test etmek gerektiğinde doğru araç gerçek tarayıcı, yani 7. yazıdaki browser mode. Geçiş yapmak istersen yöntem basit: happy-dom'u kur, jsdom'u kaldır. Builder kurulu olanı kendisi seçiyor.
Spec Dosyasının Anatomisi
isbn.ts iki saf fonksiyon içeriyor: normalizeIsbn boşluk ve tireleri atıyor, isValidIsbn13 önek ve check digit'i kontrol ediyor. Testi şöyle:
import { isValidIsbn13, normalizeIsbn } from './isbn';
describe('isValidIsbn13', () => {
it('accepts a valid ISBN-13 with hyphens', () => {
expect(isValidIsbn13('978-0-441-01359-3')).toBe(true);
});
it('rejects an ISBN-13 whose check digit is wrong', () => {
expect(isValidIsbn13('978-0-441-01359-4')).toBe(false);
});
it.each([
['too short', '978044101359'],
['letters', '978044101359X'],
['wrong prefix', '9770441013593'],
])('rejects input that is %s', (_case, input) => {
expect(isValidIsbn13(input)).toBe(false);
});
});
describe('normalizeIsbn', () => {
it('removes spaces and hyphens', () => {
expect(normalizeIsbn(' 978-0 441-01359-3 ')).toBe('9780441013593');
});
});
Yapı Jasmine'den tanıdık: describe grubu, it tek bir davranışı, expect assertion'ı kuruyor. it.each aynı davranışı farklı girdilerle çalıştırıyor ve %s her satırın ilk değerini test adına yazıyor; raporda "rejects input that is letters" gibi okunabilir isimler çıkıyor. Test isimleri method adını değil davranışı söylüyor; kalan bir testin adı, açmadan neyin bozulduğunu anlatmalı. İyi bir testin anatomisini Unit Test serisinin "İyi Bir Unit Test'in Anatomisi" yazısı .NET üzerinden anlatıyor, kurallar burada da aynı.
Testleri Çalıştırmak: Watch, CI, Tek Dosya ve Debug
Terminalde ng test watch modunda açılıyor ve dosya kaydettikçe testleri yeniden çalıştırıyor. CI'da çoğu servis CI=true ortam değişkenini tanımlıyor; builder bunu görünce tek seferlik koşuya geçiyor. CI tanımlı değilse Angular dokümanının önerdiği ng test --no-watch --no-progress aynı işi yapıyor; Bookstore'da bunu npm run test:ci script'i yaptık.
Tek bir dosyayı çalıştırmak için --include yeterli. İlginç bir ayrıntı: implementation dosyasını verirsen builder yanındaki spec'i buluyor.
$ npx ng test --no-watch --include src/app/catalog/isbn.ts
Test Files 1 passed (1)
Tests 6 passed (6)
Test adına göre süzmek için --filter bir regular expression alıyor. --reporters verbose ile atlanan testler de listede görünüyor:
$ npx ng test --no-watch --filter "rejects" --reporters verbose
↓ bookstore-web src/app/catalog/recently-viewed.spec.ts > RecentlyViewed > puts the latest book first
✓ bookstore-web src/app/catalog/isbn.spec.ts > isValidIsbn13 > rejects an ISBN-13 whose check digit is wrong 1ms
✓ bookstore-web src/app/catalog/isbn.spec.ts > isValidIsbn13 > rejects input that is too short 0ms
↓ bookstore-web src/app/app.spec.ts > App > shows the store name and the main navigation
Test Files 1 passed | 2 skipped (3)
Tests 4 passed | 6 skipped (10)
ng test --list-tests ise build yapmadan bulunan dosyaları listeliyor; include desenini değiştirdikten sonra hangi dosyaların içeride kaldığını görmenin en hızlı yolu.
Editörde tek bir testi odaklamak için it.only var, ama CI'da bırakılırsa koşu kırılıyor. CI=true ile denediğimde Vitest testi kalmış saydı: "Unexpected .only modifier. Remove it or pass --allowOnly argument to bypass this error". Bu davranış işine yarıyor: unutulan bir only, suite'in geri kalanını sessizce devre dışı bırakamıyor.
Debug için ng test --debug Node.js inspector'ı açıp bekliyor:
$ npx ng test --debug --include src/app/catalog/isbn.spec.ts
RUN v5.1.0 /home/user/bookstore-web
Debugger listening on ws://127.0.0.1:9229/<id>
For help, see: https://nodejs.org/learn/getting-started/debugging
VS Code'un Node.js debugger'ını bu porta bağlayabilir ya da Chrome'da chrome://inspect sayfasından açabilirsin. Breakpoint türleri ve source map'lerle çalışmak Browser DevTools serisinin "Sources Paneli ile JavaScript Debugging" yazısında anlatılıyor; inspector bağlandıktan sonra her şey orada anlatıldığı gibi.
Üretimde Dikkat
CI'daki test komutunu açık yaz: ng test --no-watch --no-progress ya da CI=true ng test. Watch modunda kalan bir test komutu CI job'ını timeout'a kadar bekletir. Pipeline'a test raporu ve coverage yüklemek, Playwright ile birlikte çalıştırmak GitHub Actions serisinin "GitHub Actions ile Angular CI" yazısının konusu.
isolate kararını ölçerek ver. Bookstore'un deneme suite'inde isolation süreyi 2,5 katına çıkardı. Suite büyüdükçe bu fark dakikalara dönüşüyor. Varsayılanı açık bırakıp testlerin state paylaşmamasına dikkat etmek daha ucuz. --isolate'i kalıcı ayar olarak değil, teşhis aracı olarak kullan: suite isolation'la geçip isolation'sız kalıyorsa testlerin bir şeyi paylaşıyor demektir.
Lokal yeşilin CI'da da yeşil olacağını varsayma. Worker sayısı makineye göre değişiyor ve isolation kapalıyken bu, hangi dosyaların aynı process'i paylaştığını değiştiriyor. Lokal makinede CI'ı taklit etmek için vitest-base.config.mts'e geçici olarak maxWorkers: 1 yazmak işe yarıyor.
Sürümleri birlikte yükselt. @angular/build 22.2'nin peer aralığı vitest için ^4.0.8 || ^5.0.0, TypeScript için >=6.0 <6.1. Vitest'in major sürümünü Angular'dan bağımsız yükseltmeden önce bu aralığa bak. @vitest/coverage-v8'in sürümü de vitest ile aynı olmalı.
Sık Düşülen Tuzaklar
CLI'ın yönettiği ayarları Vitest config'inde değiştirmeye çalışmak. include, projects ve watch builder tarafından eziliyor ya da siliniyor; uyarı build çıktısının arasında kaybolabiliyor. Test dosyası seçimi angular.json'da ya da komut satırında yapılır.
Module seviyesinde mutable state bırakmak. Yazının başındaki hata, isolation kapalı olduğu sürece yalnız worker sayısına bağlı olarak ortaya çıkıyor. Module içindeki let, array ya da Map testler arasında taşınabilir; injector'a bağlı bir service ise TestBed ile her testte sıfırlanıyor.
Testleri implementation dosyasının yapısına göre gruplamak. Her private method'a bir describe açmak, kod refactor edildiğinde testleri de baştan yazdırıyor. describe'ları kullanıcının ya da çağıranın gördüğü davranışa göre kur: "rejects an ISBN-13 whose check digit is wrong" refactor'dan sonra da doğru kalır.
happy-dom'u fark etmeden devreye almak. Builder kurulu olan happy-dom'u jsdom'a tercih ediyor. Bir başka araç için happy-dom'u devDependency olarak eklediğinde test ortamı sessizce değişiyor; yukarıdaki denemede innerText'in davranışı bile farklıydı.
Küçük Bir Deneme
★☆☆ isbn.spec.ts'e "accepts an ISBN-13 that starts with 979" testini ekle. Geçerli bir 979 ISBN'i bulmak için check digit'i kendin hesapla, sonra --include src/app/catalog/isbn.ts ile yalnız bu dosyayı çalıştır.
★★☆ RecentlyViewed'ı yazının başındaki module hâline geri çevir ve testleri --isolate ile ve onsuz on kez çalıştır. vitest-base.config.mts'e maxWorkers: 1 eklediğinde sonuç nasıl değişiyor? Hangisi sana hatayı her seferinde gösteriyor?
★★★ happy-dom'u kur ve jsdom'u kaldır. Suite'in süresini üç kez ölç, sonra App testine innerText kullanan bir assertion ekle ve iki ortamda sonucu karşılaştır.
Test altyapısı hazır; artık Bookstore'un asıl koduna geçebiliriz. Bir sonraki yazıda katalogu getiren CatalogService'i, fiyatı Türkçe locale ile biçimlendiren PricePipe'ı ve httpResource testlerini yazıyoruz: HttpTestingController ile istekleri yakalamak, signal ve resource durumlarını assert etmek ve debounce için fake timer'lar.