V-Model ve Test Seviyeleri: Her Geliştirme Adımının Bir Test Karşılığı
V-Model'de test sonda yapılmaz, sol kolda tasarlanır: requirements acceptance test'le, system design system test'le, mimari integration test'le, modül tasarımı unit test'le eşleşir. Bookstore'un 14 günlük iade kuralını xUnit v3 trait'leriyle requirement'a bağlıyor, tek komutla REQ-RET-01'e ait dört testi listeliyor ve bir sınır hatasının hangi requirement'ı kırdığını gerçek test çıktısında görüyoruz.
Sürüm notu: Bu yazıdaki kodlar .NET 10, xUnit v3 4.0.2 ve Microsoft.AspNetCore.Mvc.Testing 10.0.14 ile derlenip denenmiştir.
Önce Tahmin Et
V-Model'in klasik çizimine bakan çoğu geliştirici şu sonuca varır: sol kolda tasarım yapılıyor, en altta kod yazılıyor, sağ kolda da sırayla test ediliyor. Yani test yine en sonda. Şimdi tahmin et: V-Model'e göre Bookstore'un iade feature'ının acceptance test'i ne zaman yazılmalı? Kod bittikten sonra mı, system test'ten sonra mı, yoksa başka bir zaman mı?
Doğru cevap "requirements yazılırken". V'nin iki kolu arasındaki yatay çizgiler, bu modelin asıl söylediği şeyi gösteriyor ve çoğu çizimde en ince çizgiler onlar.
V-Model Nereden Geldi?
V şeklindeki modelin iki ayrı kökü var. Biri Almanya'daki V-Modell: 1980'lerin ortasında IABG (Industrieanlagen-Betriebsgesellschaft) tarafından Alman Savunma Bakanlığı için geliştirildi, sonra Alman kamu kurumlarının BT (bilgi teknolojileri) projeleri için standart hâline geldi. V-Modell 97 sürümünün yerini Şubat 2005'te V-Modell XT (eXtreme Tailoring) aldı. Diğer kök sistem mühendisliği tarafında: Kevin Forsberg ve Harold Mooz, 1991'de NCOSE (National Council on Systems Engineering) sempozyumunda sistem mühendisliği ile proje döngüsü arasındaki ilişkiyi V şekliyle anlatan bir bildiri sundu.
İki kök de aynı fikre varıyor: sistemi yukarıdan aşağıya ayrıştırırken (sol kol) her seviyede "bunu nasıl doğrulayacağım?" sorusunu cevaplamak ve sistemi aşağıdan yukarıya birleştirirken (sağ kol) her seviyeyi o cevaba göre test etmek.
Verification ve Validation
V-Model'i anlamanın anahtarı iki kelime. Barry Boehm'in sık alıntılanan formülasyonuyla verification "ürünü doğru mu yapıyoruz?" sorusunu, validation ise "doğru ürünü mü yapıyoruz?" sorusunu sorar.
Verification, bir adımın çıktısının o adımın girdisine uygun olup olmadığını kontrol eder: modül tasarımda söylendiği gibi mi davranıyor, bileşenler mimaride çizildiği gibi mi konuşuyor? Validation ise ürünün gerçek ihtiyacı karşılayıp karşılamadığını sorar: müşteri gerçekten iade yapabiliyor mu ve bu, istediği şey miydi? Bir ürün bütün verification adımlarını geçip yine de validation'da kalabilir. Requirements'ı kusursuz uygulanmış ama yanlış yazılmış bir feature tam olarak budur.

Dört Eşleşme: Bookstore'un İade Feature'ında
Requirements ile acceptance test eşleşir. Requirement (REQ-RET-01): "Müşteri, teslim aldığı siparişi teslimattan sonraki 14 gün içinde iade talebine dönüştürebilir." Requirements yazılırken bunun iki acceptance criteria'sı (AC) da yazılır: AC-1, süre içindeki talep kabul edilir ve 201 döner; AC-2, süre dışındaki talep 422 ile ve okunabilir bir mesajla reddedilir. Bu iki cümle, acceptance test'in tasarımıdır.
System design ile system test eşleşir. System design, iade akışının uçtan uca nasıl çalışacağını söyler: talep alınır, kayıt oluşur, para iadesi mesajı gönderilir. System test bu akışı gerçek bileşenlerle, production'a benzer bir ortamda dener.
Architecture design ile integration test eşleşir. Mimari, endpoint'in hangi service'leri ve hangi veritabanını kullandığını söyler; integration test bu parçaların gerçekten birlikte çalıştığını doğrular. Aşağıdaki örnekte bu seviye, uygulamayı bellekte ayağa kaldıran WebApplicationFactory ile test ediliyor.
Module design ile unit test eşleşir. Modül tasarımı iade kuralını tek bir yerde toplar; unit test o kuralın sınırlarını dener. 14. gün dahil mi, değil mi? Bu soru modül tasarımında cevaplanmazsa, unit test de yazılamaz.
Test seviyelerinin derinliği, yani hangi davranışın hangi seviyede test edilmesi gerektiği, Test Stratejisi serisinin "Neyi Hangi Seviyede Test Etmeli?" yazısında. Burada odaklandığımız şey, eşleşmenin kendisi ve onun kodda nasıl izlenebilir tutulacağı.
Kodda Traceability: Requirement'tan Teste
Modül tasarımının çıktısı, kuralı tek başına taşıyan küçük bir class:
namespace Bookstore.Returns;
// REQ-RET-01: müşteri, teslimattan sonraki 14 gün içinde iade talep edebilir
public static class ReturnPolicy
{
public static readonly TimeSpan Window = TimeSpan.FromDays(14);
public static bool CanReturn(DateTimeOffset deliveredAt, DateTimeOffset now) =>
now - deliveredAt <= Window;
}
Unit test'ler sınırı dener ve bağlı oldukları requirement'ı bir xUnit trait'i olarak taşır:
public class ReturnPolicyTests
{
private static readonly DateTimeOffset Delivered = new(2026, 11, 1, 12, 0, 0, TimeSpan.Zero);
[Fact]
[Trait("Requirement", "REQ-RET-01")]
public void CanReturn_OnDay14_IsAllowed() =>
Assert.True(ReturnPolicy.CanReturn(Delivered, Delivered.AddDays(14)));
[Fact]
[Trait("Requirement", "REQ-RET-01")]
public void CanReturn_OneMinuteAfterDay14_IsRejected() =>
Assert.False(ReturnPolicy.CanReturn(Delivered, Delivered.AddDays(14).AddMinutes(1)));
}
Integration test'ler endpoint'i gerçek pipeline'ıyla çağırır. Zamanı sabitlemek için TimeProvider'ı test içinde değiştiriyoruz; böylece test, bugünün tarihine bağlı olmadan her gün aynı sonucu verir. Her test hem requirement'ı hem de doğruladığı acceptance criteria'yı taşır:
public sealed class FixedClock(DateTimeOffset now) : TimeProvider
{
public override DateTimeOffset GetUtcNow() => now;
}
public class ReturnEndpointTests(WebApplicationFactory<Program> factory)
: IClassFixture<WebApplicationFactory<Program>>
{
private HttpClient CreateClient() =>
factory.WithWebHostBuilder(b => b.ConfigureServices(s =>
s.AddSingleton<TimeProvider>(new FixedClock(new DateTimeOffset(2026, 11, 8, 9, 0, 0, TimeSpan.Zero)))))
.CreateClient();
[Fact]
[Trait("Requirement", "REQ-RET-01")]
[Trait("Acceptance", "AC-2")]
public async Task Post_AfterWindow_Returns422()
{
HttpResponseMessage response = await CreateClient().PostAsync("/orders/1002/returns", null, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode);
}
}
AC-1 için Post_WithinWindow_Returns201 testi de aynı şekilde yazılı. Şimdi traceability'nin asıl faydası: bir requirement'a bağlı bütün testleri, hangi seviyede olursa olsun, tek komutla listeleyebiliyoruz. xUnit v3 test projeleri kendi başına çalışan uygulamalar olduğu için runner seçeneklerini doğrudan verebiliyoruz:
$ dotnet run --project tests/Bookstore.Returns.Tests -- -list tests -trait "Requirement=REQ-RET-01"
xUnit.net v3 In-Process Runner v4.0.2+0c7aa4991f (64-bit .NET 10.0.13)
Bookstore.Returns.Tests.ReturnEndpointTests.Post_AfterWindow_Returns422
Bookstore.Returns.Tests.ReturnEndpointTests.Post_WithinWindow_Returns201
Bookstore.Returns.Tests.ReturnPolicyTests.CanReturn_OnDay14_IsAllowed
Bookstore.Returns.Tests.ReturnPolicyTests.CanReturn_OneMinuteAfterDay14_IsRejected
Uygulamayı çalıştırıp AC-2'nin anlattığı durumu elle denediğimizde, 1 Ekim'de teslim edilmiş 1002 numaralı sipariş için cevap şu:
$ curl -s -i -X POST http://127.0.0.1:5481/orders/1002/returns
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
Date: Sun, 08 Nov 2026 08:58:02 GMT
Server: Kestrel
Transfer-Encoding: chunked
{"type":"https://tools.ietf.org/html/rfc4918#section-11.2","title":"Return window closed","status":422,"detail":"Order 1002 was delivered more than 14 days ago."}
Bu eşleşmeleri bir tabloda toplamak, V'nin yatay çizgilerini somut hâle getirir:
| Requirement / kriter | V seviyesi | Test |
|---|---|---|
| REQ-RET-01, AC-1: süre içinde talep 201 | Acceptance (integration ile otomatik) | Post_WithinWindow_Returns201 |
| REQ-RET-01, AC-2: süre dışında talep 422 | Acceptance (integration ile otomatik) | Post_AfterWindow_Returns422 |
| REQ-RET-01: 14. gün dahil | Unit | CanReturn_OnDay14_IsAllowed |
| REQ-RET-01: 14 gün + 1 dakika hariç | Unit | CanReturn_OneMinuteAfterDay14_IsRejected |
Tablodaki ilk iki satırda acceptance criteria'yı integration test seviyesinde otomatikleştirdik. Bu bir kısayol: gerçek acceptance, müşterinin ya da Product Owner'ın feature'ı kendi senaryosuyla denemesiyle olur. Otomatik test, o denemenin her deploy'da tekrar edilebilen kısmıdır. WebApplicationFactory'nin kendisi ASP.NET Core Web API (Application Programming Interface) serisinin "ASP.NET Core Web API'de Integration Test: WebApplicationFactory ile Uçtan Uca Test" yazısında ayrıntılı anlatılıyor.
Bir Sınır Hatası Requirement'a Nasıl Geri Döner?
Traceability'nin değeri bir şey bozulduğunda görünür. Bir refactoring sırasında birisi <='i < yapıyor; kod derleniyor, değişiklik masum görünüyor. Test koşusu şunu söylüyor:
failed Bookstore.Returns.Tests.ReturnPolicyTests.CanReturn_OnDay14_IsAllowed (0ms)
Assert.True() Failure
Expected: True
Actual: False
Test run summary: Failed!
total: 4
failed: 1
succeeded: 3
Kırılan test'in trait'i REQ-RET-01. Review eden kişi artık "bir test kırıldı" değil, "14. günün dahil olduğu requirement'ı kırdık" diye konuşur. Integration test'ler bu hatayı yakalamadı, çünkü sabitlediğimiz tarihler sınırın tam üzerinde değildi. Bu da V'nin alt seviyelerinin neden gerekli olduğunu gösteriyor: sınır değerleri en ucuz unit seviyesinde denenir.
Regüle Sektörlerde V-Model
V-Model'in en uzun yaşadığı yerler, her requirement'ın bir testle eşleştiğini bir denetçiye kanıtlamanın zorunlu olduğu sektörler. Tıbbi cihaz yazılımı için IEC (International Electrotechnical Commission) 62304, yazılım yaşam döngüsü process'lerini ve bunların belgelenmesini tarif eder; 2006 tarihli standardın 2015'te bir değişikliği yayımlandı. Otomotivde VDA (Verband der Automobilindustrie) QMC (Qualitäts Management Center) tarafından yayımlanan Automotive SPICE (Software Process Improvement and Capability dEtermination), 2023'te çıkan 4.0 sürümüyle tedarikçilerin süreç olgunluğunu değerlendirmek için kullanılır ve sol kol ile sağ kol arasındaki eşleşmeyi açıkça arar.
Bu standartların ayrıntısı bu yazının konusu değil. Önemli olan şu: bu sektörlerde traceability bir tercih değil, ürünü piyasaya çıkarmanın şartı. Bu yüzden yukarıdaki trait yaklaşımı gibi, eşleşmeyi koddan otomatik üreten yöntemler el ile tutulan tablolardan çok daha güvenilir. Testler değiştikçe tablo kendiliğinden güncel kalır.
Üretimde Dikkat
Requirement kimliklerini tek bir yerden yönet. Requirement'lar bir backlog aracında duruyorsa, test trait'indeki kimlik o araçtaki kimlikle aynı olmalı; aksi halde traceability kâğıt üzerinde kalır. CI (Continuous Integration) pipeline'ında trait'lere göre filtreli koşular, belirli bir requirement değiştiğinde yalnız ilgili testlerin hızlıca çalıştırılmasını da sağlar.
Testi tasarlarken requirement'ın belirsizliğini kayda geçir. "14 gün" yazan bir requirement, saat dilimini ve günün hangi anından itibaren sayılacağını söylemiyorsa, unit test yazmaya çalışan kişi bu soruyu ilk soran olur. Sol kolda test tasarlamanın asıl getirisi bu: belirsizlik kod yazılmadan önce yakalanır.
Zamana bağlı kuralı test etmek için sistem saatine doğrudan bağlanma. TimeProvider gibi bir soyutlama olmadan yazılmış bir integration test, takvim ilerledikçe bir gün kendiliğinden kırmızıya döner.
Sık Düşülen Tuzaklar
V-Model'i "test en sonda" diye okumak en yaygın tuzak. Modelin sağ kolu test koşusunu gösterir, test tasarımını değil. Acceptance test'i requirements'la birlikte tasarlamayan bir ekip, V'yi değil, iki kolu birbirine bağlanmamış bir Waterfall'ı uyguluyordur.
İkincisi, V-Model'in kendisini Waterfall'dan farklı bir yaşam döngüsü sanmak. Klasik V-Model de sıralıdır; geri bildirim hâlâ geç gelir. Modelin katkısı test tasarımını öne çekmek; iterasyon eklemez. Regüle ekiplerin çoğu bugün V'yi tek bir büyük geçiş olarak değil, her increment'te tekrarlanan küçük V'ler olarak uygular.
Üçüncüsü, eşleşmeyi bire bir zorunluluk gibi uygulamak. Her requirement'ın her seviyede testi olması gerekmez; bazı kurallar yalnız unit seviyesinde, bazı akışlar yalnız system seviyesinde anlamlıdır.
Dördüncüsü, traceability tablosunu elle tutmak. Elle tutulan bir tablo ilk sprint sonunda eskir; denetimden önceki hafta yeniden yazılır ve kimseye güven vermez.
Küçük Bir Deneme
★☆☆ Ekibinin son feature'ından bir requirement seç ve dört seviyenin her biri için "bu seviyede neyi test ederdim?" sorusunu cevapla. Hangi seviye boş kaldı?
★★☆ Yukarıdaki örneğe kısmi iade için REQ-RET-02 ekle: beş kitaplık bir siparişten yalnız iki kitap iade edilebilsin. Önce acceptance criteria'yı yaz, sonra trait'li testleri, en son kodu. -trait "Requirement=REQ-RET-02" filtresi kaç test listeliyor?
★★★ Test projesinin keşfettiği testleri trait'leriyle birlikte okuyup bir traceability tablosunu Markdown olarak üreten küçük bir script yaz (-list full çıktısı iyi bir başlangıç). Bu tabloyu CI'da artifact olarak saklarsan denetimde ne kazanırsın?
V-Model testi öne çekti ama süreci hâlâ tek geçişte bıraktı. Bir sonraki yazıda süreci döngüye çeviren modellere bakıyoruz: incremental ve iterative geliştirme arasındaki fark, Boehm'in risk odaklı Spiral modeli ve Bookstore checkout'unun bir walking skeleton'la başlaması.