White-box ve Experience-based Teknikler: Branch Coverage'ı Test Tasarımı için Kullanmak, Error Guessing, Checklist ve Exploratory Testing
Branch coverage raporu test tasarımında hedef değil, hiç çalışmamış dalı gösterip yeni test case'ler çıkaran bir araçtır; ama yazılmamış kodu göremez, onu error guessing, checklist ve exploratory testing bulur. Bookstore'un kupon kodunda rapor story'de olmayan tavan dalındaki hatayı, error guessing Türkçe I hatasını, bir saatlik exploratory session de çift tıklamayla açılan iki siparişi ve negatif tutarlı siparişi buldu; hepsi regression testine döndü.
Sürüm notu: Bu yazıdaki kodlar .NET 10, ASP.NET Core 10, xUnit.net v3 4.0.3 ve Microsoft.Testing.Extensions.CodeCoverage 18.12.0 ile derlenip denenmiştir.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'da (bir online kitapçı) yeni bir kupon var: KITAP10. Selin'in story'si kısa: en az 200 TL'lik sepette yüzde 10 indirim; kurumsal müşteride kupon geçmez, çünkü kurumsal indirimle birleşmez. Emre'nin kodu:
namespace Bookstore.Ordering;
public sealed record Coupon(string Code, decimal Percent, decimal MinimumSubtotal, decimal? MaxDiscount = null);
public static class CouponCodes
{
public static string Normalize(string code) => code.Trim().ToUpper();
}
public static class CouponCalculator
{
public static decimal Discount(Coupon coupon, decimal subtotal, CustomerType customer)
{
if (customer == CustomerType.Institutional)
{
return 0m; // kurumsal indirimle birleşmez
}
if (subtotal < coupon.MinimumSubtotal)
{
return 0m;
}
decimal discount = Math.Round(subtotal * coupon.Percent / 100m, 2, MidpointRounding.AwayFromZero);
if (coupon.MaxDiscount is decimal max && discount > max)
{
discount = coupon.Percent;
}
return discount;
}
}
Ayşe story'den testleri önceki yazılardaki tekniklerle çıkarmış: minimum tutarın iki yanı, bir misafir müşteri, bir kurumsal müşteri.
// Story'den: en az 200 TL'lik sepette yüzde 10; kurumsal müşteride kupon geçmez
public static TheoryData<CustomerType, decimal, decimal> StoryCases => new()
{
{ CustomerType.Individual, 199.99m, 0m },
{ CustomerType.Individual, 200.00m, 20.00m },
{ CustomerType.Guest, 350.00m, 35.00m },
{ CustomerType.Institutional, 350.00m, 0m },
};
[Theory]
[MemberData(nameof(StoryCases))]
public void Discount_follows_the_story(CustomerType customer, decimal subtotal, decimal expected)
{
Assert.Equal(expected, CouponCalculator.Discount(Kitap10, subtotal, customer));
}
Testler yeşil. 2.500 TL'lik bir sepette KITAP10 ne kadar indirim yapar?
Cevap: Story'de Olmayan Bir Dal
10 TL. Kampanya ekibi kuponu tanımlarken "en fazla 100 TL" diye bir tavan eklemiş, Emre de bunu koda koymuş, ama story'ye kimse yazmamış. Tavanı aşan indirimde kod tavanı değil yüzde oranını döndürüyor: 250 TL'lik indirim 100 TL'ye inmesi gerekirken 10 TL'ye iniyor.
Story'den türetilen testler bu dalı göremezdi, çünkü story'de böyle bir kural yok. Önceki altı yazının bütün teknikleri black-box: test case'leri requirement'tan, modelden, tablodan çıkarıyorlar. Requirement eksikse onlar da eksik kalır. Bu yazıda iki farklı kaynağa bakıyoruz: koda (white-box) ve test edenin deneyimine (experience-based).
Branch Coverage'ı Test Tasarımı için Okumak
ISTQB (International Software Testing Qualifications Board; yazılım test sertifikalarını tanımlayan uluslararası kurul) CTFL (Certified Tester Foundation Level; temel seviye test sertifikası) v4.0.1 syllabus'ı iki white-box teknik anlatır. Statement testing'de coverage item'lar çalıştırılabilir satırlardır. Branch testing'de coverage item'lar branch'lerdir, yani kodun control flow'unda bir noktadan diğerine geçişler: bir if'in doğru ve yanlış tarafı, bir switch'in her kolu, bir döngüye girme ya da çıkma kararı. Yüzde 100 branch coverage yüzde 100 statement coverage'ı da sağlar, tersi doğru değildir. Coverage yüzdesinin ne anlama geldiği Test Stratejisi serisinin "Code Coverage Ne Söyler, Ne Söylemez" yazısının konusu; burada raporu yalnızca bir şey için kullanıyoruz: hiç çalışmamış dalı bulup ondan test case çıkarmak.
xUnit v3 testleri Microsoft.Testing.Platform üzerinde koştuğu için coverage'ı Microsoft'un Microsoft.Testing.Extensions.CodeCoverage paketi topluyor. Paketi test projesine ekleyip Cobertura formatında rapor istiyoruz:
> dotnet test --project tests/Bookstore.Ordering.Tests --coverage --coverage-output-format cobertura --coverage-output coverage.cobertura.xml
In process file artifacts produced:
- <repo>/TestResults/coverage.cobertura.xml
Test run summary: Passed!
total: 108
failed: 0
succeeded: 108
skipped: 0
duration: 686ms
Raporda CouponCalculator sınıfının satırları:
<class line-rate="0.7857142857142857" branch-rate="0.75" complexity="8" name="Bookstore.Ordering.CouponCalculator" filename="<repo>/src/Bookstore.Ordering/CouponCalculator.cs">
<lines>
<line number="24" hits="1" branch="False" />
<line number="25" hits="1" branch="True" condition-coverage="50% (2/4)">
<conditions>
<condition number="0" type="jump" coverage="50%" />
<condition number="1" type="jump" coverage="50%" />
</conditions>
</line>
<line number="26" hits="0" branch="False" />
<line number="27" hits="0" branch="False" />
<line number="28" hits="0" branch="False" />
25. satır if (coupon.MaxDiscount is decimal max && discount > max). İki koşul var, her birinin iki çıkışı; dört branch'ten ikisi hiç çalışmamış. 26–28. satırlar, yani if'in gövdesi, hiç çalışmamış (hits="0"). Raporu bir test case listesine çeviriyoruz: birinci koşul hep doğru çalışmış, çünkü testlerdeki kuponun her zaman bir tavanı var; tavanı olmayan bir kupon denenmemiş. İkinci koşul hep yanlış çalışmış; tavanı aşan bir indirim hiç denenmemiş.
Rapor bize "şu satırı çalıştır" diyor, ama beklenen sonucu söylemiyor. Beklenen sonucu kodu okuyarak yazmak, koddaki hatayı teste kopyalamak olur. Bu yüzden ilk iş Selin'e gidip tavanı sormak: "en fazla 100 TL" kuralı doğru mu ve story'de neden yok? Cevap gelince sınırı önceki yazıdaki gibi boundary value analysis ile test ediyoruz: indirimin tam 100 TL olduğu 1.000,00 TL, tavanı bir kuruş aşan 1.000,10 TL ve açıkça aşan 2.500 TL.
// Coverage raporundan: tavanı aşan indirim ve tavanı olmayan kupon hiç çalışmamıştı
public static TheoryData<decimal, decimal> CapCases => new()
{
{ 1000.00m, 100.00m },
{ 1000.10m, 100.00m },
{ 2500.00m, 100.00m },
};
[Theory]
[MemberData(nameof(CapCases))]
public void Discount_is_capped_at_the_maximum(decimal subtotal, decimal expected)
{
Assert.Equal(expected, CouponCalculator.Discount(Kitap10, subtotal, CustomerType.Individual));
}
[Fact]
public void Coupon_without_a_maximum_is_not_capped()
{
Coupon uncapped = Kitap10 with { MaxDiscount = null };
Assert.Equal(250.00m, CouponCalculator.Discount(uncapped, 2500.00m, CustomerType.Individual));
}
failed Bookstore.Ordering.Tests.CouponCalculatorTests.Discount_is_capped_at_the_maximum(subtotal: 1000.10, expected: 100.00) (1ms)
Assert.Equal() Failure: Values differ
Expected: 100.00
Actual: 10
failed Bookstore.Ordering.Tests.CouponCalculatorTests.Discount_is_capped_at_the_maximum(subtotal: 2500.00, expected: 100.00) (0ms)
Assert.Equal() Failure: Values differ
Expected: 100.00
Actual: 10
Test run summary: Failed!
total: 8
failed: 2
succeeded: 6
skipped: 0
duration: 317ms
Düzeltme tek kelime:
discount = max;
Düzeltmeden sonra CouponCalculator'ın line ve branch coverage'ı yüzde 100.
White-box'ın Göremediği: Eksik Kod
Aynı raporda iki satır daha var. CouponCodes sınıfının coverage'ı yüzde 100, CheckoutService'inki de yüzde 100. İkisinde de hata var, birazdan göreceğiz. Bu çelişki değil; syllabus white-box tekniklerin zayıf tarafını açıkça söyler: implementasyonun tamamını hesaba kattıkları için belirsiz ya da eski bir spesifikasyonda bile hata bulabilirler, ama kodda hiç yazılmamış bir şeyi göremezler. Boş sepeti reddeden bir if yoksa, onun çalışmamış bir branch'i de yoktur; rapor yüzde 100 der.
Eksik kodu bulmak için koda değil, yazılımın nasıl bozulduğuna dair bilgiye bakmak gerekiyor. Syllabus bunu experience-based teknikler altında topluyor: error guessing, checklist-based testing ve exploratory testing.
Error Guessing ve Hata Taksonomisi
Error guessing, test edenin bilgisine dayanarak hata, defect ve failure'ları önceden tahmin etmektir: uygulamanın geçmişte nasıl davrandığı, geliştiricilerin sık yaptığı hatalar, benzer uygulamalarda görülen failure'lar. Syllabus bu bilgiyi girdi, çıktı, mantık, hesaplama, interface ve veri gibi kategorilerle düzenler; sistematik hâline fault attack denir: olası hataların bir listesini çıkarıp her birini ortaya çıkaracak testleri tasarlamak.
Bookstore ekibinin listesi, bu serideki ve önceki projelerdeki hatalardan derlenmiş:
- Null, boş string, boş liste; sıfır ve negatif adet.
- Türkçe I: `ToUpper()` ve `ToLower()` current culture'a bağlıdır; tr-TR'de "i" büyüyünce "İ" olur.
- Time zone: UTC (Coordinated Universal Time; dünya genelinde referans alınan saat) ile İstanbul saati arasındaki üç saat, gün sınırında tarih kayması (üçüncü yazı).
- Eşzamanlı istek: aynı siparişe aynı anda gelen iki event (beşinci yazı).
- Çift tıklama ve retry: aynı isteğin iki kez gelmesi.
- Para: yuvarlama, negatif toplam, kuruş kaybı.
CouponCodes.Normalize listenin ikinci maddesine birebir uyuyor. Bookstore'un API'si fiyatları Türkiye için biçimlendirdiği için tr-TR culture ile çalışıyor; müşteri kuponu küçük harfle "kitap10" yazarsa ToUpper() "KİTAP10" üretir ve kupon bulunamaz. Story bunu söylemiyor, coverage raporu yüzde 100, ama listedeki madde doğrudan bir test case:
using System.Globalization;
namespace Bookstore.Ordering.Tests;
public sealed class CouponCodeTests
{
// Error guessing: Türkçe I, büyük/küçük harf ve baştaki/sondaki boşluk
[Theory]
[InlineData("en-US", "kitap10")]
[InlineData("tr-TR", "kitap10")]
[InlineData("tr-TR", " Kitap10 ")]
[InlineData("tr-TR", "KITAP10")]
public void Coupon_code_is_normalized_the_same_way_in_every_culture(string culture, string code)
{
CultureInfo original = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(culture);
Assert.Equal("KITAP10", CouponCodes.Normalize(code));
}
finally
{
CultureInfo.CurrentCulture = original;
}
}
}
failed Bookstore.Ordering.Tests.CouponCodeTests.Coupon_code_is_normalized_the_same_way_in_every_culture(culture: "tr-TR", code: "kitap10") (11ms)
Assert.Equal() Failure: Strings differ
↓ (pos 1)
Expected: "KITAP10"
Actual: "KİTAP10"
↑ (pos 1)
failed Bookstore.Ordering.Tests.CouponCodeTests.Coupon_code_is_normalized_the_same_way_in_every_culture(culture: "tr-TR", code: " Kitap10 ") (0ms)
Assert.Equal() Failure: Strings differ
↓ (pos 1)
Expected: "KITAP10"
Actual: "KİTAP10"
↑ (pos 1)
Test run summary: Failed!
total: 12
failed: 2
succeeded: 10
skipped: 0
duration: 378ms
Kod, kullanıcıya gösterilmeyen bir kimliği normalize ediyor; culture'dan bağımsız olmalı:
public static string Normalize(string code) => code.Trim().ToUpperInvariant();
Testte culture'ı try/finally içinde değiştirip geri almamız da bilinçli: CurrentCulture thread'e bağlı ve geri alınmazsa aynı thread'de sonra koşan testleri etkiler.
Checklist Tabanlı Test
Hata taksonomisi bir kişinin aklında kaldığı sürece error guessing'in kalitesi o kişiye bağlı. Checklist bu bilgiyi ekibin ortak malı yapar. Syllabus'ın checklist için önerileri pratik: maddeler soru olarak yazılır, her madde ayrı ve doğrudan kontrol edilebilir olur, otomatik kontrol edilebilecek şeyler listeye girmez, liste hata analizine göre düzenli güncellenir ve fazla uzamasına izin verilmez. Checkout için ekibin listesi:
| Soru | Kaynağı |
|---|---|
| Boş sepet, sıfır ve negatif adet reddediliyor mu? | Boş ve sınır girdiler |
| Kupon kodu büyük/küçük harf, boşluk ve Türkçe karakterle aynı sonucu veriyor mu? | Türkçe I hatası |
| Aynı istek iki kez gelince tek sipariş mi açılıyor? | Çift tıklama, retry |
| Tarihli kurallar İstanbul saatiyle mi değerlendiriliyor? | Kampanya bitiş hatası |
| Toplam tutar negatif olabilir mi, kuruş doğru yuvarlanıyor mu? | Para hataları |
| Hata mesajı müşteriye neyin yanlış olduğunu söylüyor mu? | Müşteri hizmetleri talepleri |
Liste testlerin yerine geçmiyor; yeni bir özellik ya da code review sırasında "bunu düşündük mü?" sorusunu sistematik hâle getiriyor. Bir madde otomatik bir teste dönüştüğünde listeden çıkar ya da "testi var mı?" sorusuna dönüşür.
Exploratory Testing: Charter ve Time-box
Exploratory testing'de test tasarımı, çalıştırma ve değerlendirme aynı anda yapılır; test eden sistemi öğrendikçe bir sonraki testi seçer. "Plansız tıklama" ile farkı yapı: James ve Jon Bach'ın 2000 yılında yayımladığı Session-Based Test Management makalesi bu yapıyı tanımlar. Çalışma, kesintisiz ve süresi belli session'lara bölünür; her session'ın amacını bir charter belirler; test eden session boyunca not tutar, bulduğu hataları ve soruları ayırır; session'ın sonunda bir debriefing'de sonuçları paydaşlarla konuşur. Bach'ın debriefing için kullandığı PROOF (Past, Results, Obstacles, Outlook, Feelings; ne yapıldı, ne bulundu, ne engel oldu, sırada ne var, test eden sonuçtan emin mi) kısaltması bu konuşmanın başlıklarını verir.
Charter yazmak için Elisabeth Hendrickson'ın Explore It! (2013) kitabındaki kalıp kullanışlı: neyi keşfedeceğini, hangi kaynaklarla keşfedeceğini ve hangi bilgiyi aradığını tek cümlede söylemek. Ayşe'nin bir saatlik session'ı için charter:
"Checkout API'sini farklı sepet içerikleri, kupon yazımları ve tekrarlanan isteklerle keşfet; siparişin yanlış tutarla ya da birden fazla kez oluştuğu durumları bul."
Charter bir test case listesi değil; yön veriyor ama adımları söylemiyor. Ayşe API'yi yerelde çalıştırıp istekleri gönderiyor, her sonuca göre bir sonraki isteği seçiyor. Session'dan seçilmiş istekler ve gerçek yanıtlar:
POST /api/checkout
{"customer":"Individual","lines":[{"isbn":"9780441013593","quantity":2,"unitPrice":150.00}],"couponCode":"kitap10"}
HTTP/1.1 201 Created
Location: /api/orders/1001
{"orderId":1001,"subtotal":300.00,"discount":30.00,"total":270.00}
# Aynı istek, iki kez art arda
{"orderId":1003,"subtotal":300.00,"discount":30.00,"total":270.00}
{"orderId":1002,"subtotal":300.00,"discount":30.00,"total":270.00}
POST /api/checkout
{"customer":"Guest","lines":[],"couponCode":null}
HTTP/1.1 201 Created
Location: /api/orders/1004
{"orderId":1004,"subtotal":0,"discount":0,"total":0}
POST /api/checkout
{"customer":"Guest","lines":[{"isbn":"9780141439518","quantity":-3,"unitPrice":95.00}]}
{"orderId":1006,"subtotal":-285.00,"discount":0,"total":-285.00}
POST /api/checkout
{"customer":"Individual","lines":[{"isbn":"9780547928227","quantity":1,"unitPrice":120.00}],"couponCode":"KİTAP10"}
{"type":"https://tools.ietf.org/html/rfc4918#section-11.2","title":"Unprocessable Entity","status":422,"detail":"Unknown coupon 'KİTAP10'."}
Küçük harfli "kitap10" artık çalışıyor; error guessing düzeltmesi yerinde. Ama session başka şeyler buldu. Ayşe'nin session notları:
- Hata: aynı istek iki kez gelince iki sipariş açılıyor (1002 ve 1003). Butona çift tıklayan ya da bağlantısı kopup yeniden deneyen müşteri iki kez ödeme yapabilir.
- Hata: boş sepet 201 Created ile 0 TL'lik sipariş açıyor; adedi 0 olan satır da aynı.
- Hata: negatif adet toplamı negatif yapıyor (-285 TL). Ödeme tarafı bunu iade gibi yorumlarsa ciddi bir para kaybı.
- Soru: Türkçe klavyede büyük harfle yazılan "KİTAP10" bulunamıyor. Kupon kodlarında I harfi kullanılmalı mı, yoksa İ de kabul edilmeli mi? Selin'e.
- Soru: kurumsal müşteri kupon girince indirim sessizce 0 oluyor; yanıtta bunun sebebi yok. Selin'e.
Session sheet'te hatalar ile sorular ayrı tutuluyor, çünkü sorular kod değişikliği değil karar bekliyor. Debriefing'de Selin ikinci soruya hemen cevap veriyor: yanıtta bir uyarı olmalı, bu yeni bir story. Birinci soru kampanya ekibine gidiyor.
Session Notlarından Regression Testine
Exploratory session'ın en sık kaybedilen çıktısı, bulguların testlere dönüşmemesi. Her hatayı onu tekrar üretebilen en alt seviyede bir teste çeviriyoruz. Üç hatanın üçü de CheckoutService'te; HTTP'ye gerek yok:
// Exploratory session bulguları, regression testi olarak
[Fact]
public void Empty_cart_is_rejected()
{
CheckoutResult result = NewService().PlaceOrder(new(CustomerType.Guest, [], null));
Assert.Null(result.Order);
Assert.Equal("Cart is empty.", result.Error);
}
[Theory]
[InlineData(0)]
[InlineData(-3)]
public void Line_quantity_must_be_positive(int quantity)
{
CheckoutResult result = NewService().PlaceOrder(
new(CustomerType.Guest, [new CartLine("9780141439518", quantity, 95m)], null));
Assert.Null(result.Order);
Assert.Equal("Quantity must be positive.", result.Error);
}
[Fact]
public void Same_idempotency_key_returns_the_same_order()
{
CheckoutService service = NewService();
CheckoutCommand command = new(CustomerType.Individual, [Dune], "KITAP10");
CheckoutResult first = service.PlaceOrder(command, idempotencyKey: "checkout-7f3a");
CheckoutResult second = service.PlaceOrder(command, idempotencyKey: "checkout-7f3a");
Assert.Equal(first.Order, second.Order);
}
Çift tıklamanın çözümü bir idempotency key: istemci her checkout denemesi için bir key üretir ve isteği yeniden gönderdiğinde aynı key'i kullanır. Testi derlemek için PlaceOrder'a önce parametreyi ekleyip henüz kullanmıyoruz:
failed Bookstore.Ordering.Tests.CheckoutServiceTests.Line_quantity_must_be_positive(quantity: 0) (8ms)
Assert.Null() Failure: Value is not null
Expected: null
Actual: PlacedOrder { OrderId = 1001, Subtotal = 0, Discount = 0, Total = 0 }
failed Bookstore.Ordering.Tests.CheckoutServiceTests.Line_quantity_must_be_positive(quantity: -3) (0ms)
Assert.Null() Failure: Value is not null
Expected: null
Actual: PlacedOrder { OrderId = 1001, Subtotal = -285, Discount = 0, Total = -285 }
failed Bookstore.Ordering.Tests.CheckoutServiceTests.Empty_cart_is_rejected (0ms)
Assert.Null() Failure: Value is not null
Expected: null
Actual: PlacedOrder { OrderId = 1001, Subtotal = 0, Discount = 0, Total = 0 }
failed Bookstore.Ordering.Tests.CheckoutServiceTests.Same_idempotency_key_returns_the_same_order (1ms)
Assert.Equal() Failure: Values differ
Expected: PlacedOrder { OrderId = 1001, Subtotal = 300, Discount = 30, Total = 270 }
Actual: PlacedOrder { OrderId = 1002, Subtotal = 300, Discount = 30, Total = 270 }
Test run summary: Failed!
total: 6
failed: 4
succeeded: 2
skipped: 0
duration: 366ms
Düzeltme PlaceOrder'ın başına iki kontrol ve lock içinde key'e göre arama:
if (command.Lines.Count == 0)
{
return new CheckoutResult(null, "Cart is empty.");
}
if (command.Lines.Any(line => line.Quantity <= 0))
{
return new CheckoutResult(null, "Quantity must be positive.");
}
lock (_lock)
{
// Aynı key ile gelen ikinci istek yeni sipariş açmaz, ilk siparişi döndürür
if (idempotencyKey is not null && _ordersByIdempotencyKey.TryGetValue(idempotencyKey, out PlacedOrder? existing))
{
return new CheckoutResult(existing, null);
}
PlacedOrder order = new(_nextOrderId++, subtotal, discount, subtotal - discount);
if (idempotencyKey is not null)
{
_ordersByIdempotencyKey[idempotencyKey] = order;
}
return new CheckoutResult(order, null);
}
API key'i header'dan alıyor:
app.MapPost("/api/checkout", (
CheckoutCommand command,
[FromHeader(Name = "Idempotency-Key")] string? idempotencyKey,
CheckoutService checkout) =>
{
CheckoutResult result = checkout.PlaceOrder(command, idempotencyKey);
return result.Order is { } order
? Results.Created($"/api/orders/{order.OrderId}", order)
: Results.Problem(detail: result.Error, statusCode: StatusCodes.Status422UnprocessableEntity);
});
Session'ın bulgularını API üzerinden bir kez daha deniyoruz. Aynı Idempotency-Key: checkout-7f3a header'ıyla iki kez gönderilen istek tek sipariş döndürüyor, boş sepet ve negatif adet 422 alıyor:
{"orderId":1001,"subtotal":300.00,"discount":30.00,"total":270.00}
{"orderId":1001,"subtotal":300.00,"discount":30.00,"total":270.00}
{"type":"https://tools.ietf.org/html/rfc4918#section-11.2","title":"Unprocessable Entity","status":422,"detail":"Cart is empty."}
{"type":"https://tools.ietf.org/html/rfc4918#section-11.2","title":"Unprocessable Entity","status":422,"detail":"Quantity must be positive."}
Test run summary: Passed!
total: 149
failed: 0
succeeded: 149
skipped: 0
duration: 339ms
Bu idempotency bellekte tutulan bir dictionary ve tek bir sunucu instance'ı için çalışıyor. Birden fazla instance'ta key'in veritabanında ya da paylaşılan bir cache'te tutulması gerekir; bunun testi unit değil integration seviyesinde.
Üretimde Dikkat
Coverage raporunu pull request'te oku. Toplam yüzdeye değil, değişen dosyalarda çalışmamış branch'lere bak. Bu yazıdaki tavan dalı, yüzde 95'lik bir toplamın içinde kaybolan küçük bir kırmızı alandı.
Coverage'ı hedef yapma. Hedef bir sayı olunca testler o sayıyı yükseltmek için yazılır, beklenen sonuç da kodu okuyarak doldurulur. Rapor bir soru listesi üretir; cevapları requirement'tan ve Selin'den gelir.
Error guessing listesini canlı tut. Her production hatasının kök nedeni analiz edildiğinde "bu bir kategori mi?" diye sor. Kategoriyse listeye ve checklist'e girer; tek seferlikse yalnızca bir regression testi olur.
Exploratory session'ı takvime koy. "Vakit kalırsa keşfederiz" diye bırakılan exploratory testing hiç yapılmaz. Yeni bir özellik için bir ya da iki session, charter'larıyla birlikte sprint planına girer; debriefing'de çıkan sorular backlog'a gider.
Sık Düşülen Tuzaklar
Coverage yüzde 100 diye test tasarımını bitmiş saymak. CheckoutService ve CouponCodes yüzde 100'dü ve ikisinde de hata vardı. Coverage çalışan kodu ölçer, yazılmamış kodu ölçemez.
Beklenen sonucu koddan okumak. Coverage raporundaki boş dalı kapatmak için testin beklenen değerini koddaki ifadeye bakarak yazarsan, discount = coupon.Percent satırı da "doğrulanmış" olur. Beklenen değer kuraldan gelir.
Exploratory testing'i plansız tıklama sanmak. Charter'ı, süresi ve notları olmayan bir keşif, ertesi gün kimsenin hatırlamadığı bir öğleden sonradır. Charter yönü, time-box maliyeti, notlar da bulguların kaybolmamasını sağlar.
Bulguları testlere çevirmemek. Exploratory session'da bulunan bir hata düzeltilip regression testi yazılmazsa, aynı hata bir sonraki refactoring'de sessizce geri gelir.
Error guessing'i yalnızca deneyimli kişiye bırakmak. Taksonomi ve checklist yazılı olunca ekibe yeni katılan biri de aynı soruları sorar. Deneyim kişiden listeye taşınır.
Küçük Bir Deneme
★☆☆ Discount'taki if (customer == CustomerType.Institutional) kontrolünü sil ve coverage raporunu yeniden üret. Branch coverage ne olur? Hangi test kırmızıya döner?
★★☆ Ayşe'nin ikinci sorusunun cevabını varsay: kurumsal müşteri kupon girerse sipariş yine açılır ama yanıtta "Coupons don't apply to institutional orders." uyarısı olur. CheckoutResult'ı genişlet, önce testi yaz, sonra kodu.
★★★ Kargo ücreti (dördüncü yazı) için kendi charter'ını yaz ve 30 dakikalık bir session yap: ShippingFeeCalculator'ı bir console uygulamasından ya da küçük bir endpoint'ten çağır. Bulgularını hata ve soru olarak ayır, her hatayı bir regression testine çevir. Checklist'e yeni bir madde eklemen gerekiyor mu?
Yedi yazıda yedi teknik gördük. Son yazıda hepsini bir araya getiriyoruz: bir story geldiğinde hangi tekniği seçeceğiz, tekniklerin ürettiği testlerdeki tekrarları nasıl atacağız ve AI'dan test case istediğimizde neyi kontrol etmemiz gerekiyor. Örnek, Requirements Analysis serisinde ikinci release'e bırakılan story: kurumsal siparişte kısmi sevkiyat.