Mert Özen Her Satırda Daha İleri
Test ve Kalite Test Tasarım Teknikleri — #08

Tekniği Seçmek ve Birleştirmek: Requirement'tan Test Listesine, Acceptance Criteria ile Hizalamak ve AI ile Test Case Üretmenin Sınırları

Mert Özen 15 Kas 2026 15 dk 8 görüntülenme
Tekniği Seçmek ve Birleştirmek: Requirement'tan Test Listesine, Acceptance Criteria ile Hizalamak ve AI ile Test Case Üretmenin Sınırları

Test tekniğini requirement'ın şekli seçer: aralıklar için equivalence partitioning ve boundary value analysis, koşul kombinasyonları için decision table, geçmişe bağlı sonuç için state transition; teknikler birleştirilip tekrarları atılınca tek bir test listesi kalır. Bookstore'un kurumsal kısmi sevkiyat story'sinde beş teknik 27 condition üretti, liste 19'a indi ve üç tekniğin buluştuğu satır sevkiyat hatasını yakaladı. AI'dan test case istemek için prompt ve review checklist'i.

Sürüm notu: Bu yazıdaki kodlar .NET 10, xUnit.net v3 4.0.3 ve Microsoft.Extensions.TimeProvider.Testing 11.0.0 ile derlenip denenmiştir.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'da (bir online kitapçı) Requirements Analysis serisinin ikinci release'e bıraktığı story sırada: kurumsal siparişte kısmi sevkiyat. Selin depo ve muhasebeyle yaptığı görüşmeden sonra açık soruları kapatmış. Story'nin arka yüzündeki kurallar:

  • Kurum stok yetmediğinde kısmi sevkiyatı seçerse stokta olan kitaplar hemen gönderilir, eksikler stok geldikçe ek sevkiyatlarla gider.
  • Bir sipariş en fazla üç sevkiyata bölünür; üçüncü sevkiyat siparişi tamamlamak zorundadır.
  • Kurum kargo ücretini bir kez, ilk sevkiyatta öder; ek sevkiyatların ücretini Bookstore üstlenir.
  • Fatura her sevkiyatta yalnızca sevk edilen kitaplar için kesilir.
  • Sipariş tarihinden 30 gün sonra hâlâ gönderilmemiş kitap varsa back office kalanı iptal edip tutarını iade edebilir.
  • En az 20 adet kuralı siparişe bakar, sevkiyata değil.

Emre'nin kodu:

namespace Bookstore.Ordering;

public enum FulfillmentState
{
    Paid,
    PartiallyShipped,
    Shipped,
    ClosedWithCancellation,
}

public sealed record InstitutionalOrderLine(string Isbn, int Quantity, decimal UnitPrice);

public sealed record Shipment(int Number, IReadOnlyDictionary<string, int> Items, decimal InvoiceAmount, decimal ShippingFee);

public sealed class InstitutionalFulfillment(
    IReadOnlyList<InstitutionalOrderLine> lines, decimal shippingFee, DateTimeOffset orderedAt, TimeProvider clock)
{
    public const int MaxShipments = 3;
    public static readonly TimeSpan BackorderWindow = TimeSpan.FromDays(30);

    private readonly Dictionary<string, int> _shipped = lines.ToDictionary(line => line.Isbn, _ => 0);
    private readonly List<Shipment> _shipments = [];

    public FulfillmentState State { get; private set; } = FulfillmentState.Paid;

    public IReadOnlyList<Shipment> Shipments => _shipments;

    public int Remaining(string isbn) => lines.Single(line => line.Isbn == isbn).Quantity - _shipped[isbn];

    public Shipment Ship(IReadOnlyDictionary<string, int> items)
    {
        if (State is not (FulfillmentState.Paid or FulfillmentState.PartiallyShipped))
        {
            throw new InvalidOperationException($"Cannot ship an order that is {State}.");
        }

        if (items.Count == 0)
        {
            throw new ArgumentException("A shipment must contain at least one book.", nameof(items));
        }

        foreach ((string isbn, int quantity) in items)
        {
            if (!_shipped.ContainsKey(isbn))
            {
                throw new ArgumentException($"{isbn} is not in this order.", nameof(items));
            }

            if (quantity <= 0 || quantity > Remaining(isbn))
            {
                throw new ArgumentException($"Quantity for {isbn} must be between 1 and {Remaining(isbn)}.", nameof(items));
            }
        }

        int number = _shipments.Count + 1;
        bool completesOrder = items.Values.Sum() == lines.Sum(line => line.Quantity);
        if (number == MaxShipments && !completesOrder)
        {
            throw new InvalidOperationException($"Shipment {MaxShipments} must complete the order.");
        }

        foreach ((string isbn, int quantity) in items)
        {
            _shipped[isbn] += quantity;
        }

        decimal invoice = items.Sum(item => item.Value * lines.Single(line => line.Isbn == item.Key).UnitPrice);
        Shipment shipment = new(number, items, invoice, number == 1 ? shippingFee : 0m);
        _shipments.Add(shipment);
        State = completesOrder ? FulfillmentState.Shipped : FulfillmentState.PartiallyShipped;
        return shipment;
    }

    public decimal CancelRemaining()
    {
        if (State is not (FulfillmentState.Paid or FulfillmentState.PartiallyShipped))
        {
            throw new InvalidOperationException($"Cannot cancel the rest of an order that is {State}.");
        }

        if (clock.GetUtcNow() - orderedAt < BackorderWindow)
        {
            throw new InvalidOperationException("Remaining books can be cancelled only after 30 days.");
        }

        decimal refund = lines.Sum(line => (line.Quantity - _shipped[line.Isbn]) * line.UnitPrice);
        State = FulfillmentState.ClosedWithCancellation;
        return refund;
    }
}

Bu serideki yedi tekniğin hepsini bu story'ye uygulayabilirsin. Hangilerini seçersin, kaç test yazarsın? Kodda bir hata var; onu hangi teknik bulur?

Cevap: Hata İki Tekniğin Kesiştiği Yerde

completesOrder yalnızca bu sevkiyatın siparişin tamamı olup olmadığına bakıyor, daha önce gönderilenleri saymıyor. Sonuç iki yerde görünüyor: ilk sevkiyatta Dune'ları, ikincide Emma'ları gönderen sipariş Shipped olmuyor, PartiallyShipped'de kalıyor; üç sevkiyata bölünmüş bir sipariş ise üçüncü sevkiyatta "siparişi tamamlamalı" hatası alıyor, oysa tam da siparişi tamamlıyor.

Bu hatayı bulan test, state transition testing'in PartiallyShipped'den Shipped'e geçişi ile boundary value analysis'in "kalan adedin tamamı" sınır değerinin aynı testte buluştuğu yer. Decision table'ın "ikinci sevkiyat siparişi tamamlıyor" kuralı da aynı test. Üç teknik aynı test case'i üretiyor. Bu yazının asıl konusu bu: teknikleri birleştirince tekrarları atmak, ve bir story için hangi tekniğin gerçekten iş gördüğüne karar vermek.

Karar Rehberi: Hangi Requirement'a Hangi Teknik?

Teknik seçimini requirement'ın şekli belirler. Story'nin cümlelerine bakıp hangi soruyu sorduğunu bulmak, tekniği büyük ölçüde seçiyor:

Requirement'ta ne var?TeknikSerideki yazı
Değer aralıkları, tipler, formatlarEquivalence partitioning02
Sıralı aralıkların sınırları: adet, tutar, tarih, uzunlukBoundary value analysis03
Sonucu birden fazla koşulun kombinasyonu belirliyorDecision table testing04
Sonuç geçmişe bağlı: state, event, sıraState transition testing05
Birbirinden bağımsız görünen çok sayıda parametrePairwise testing06
Kodda requirement'ta olmayan dallar olabilirBranch coverage ile eksik test arama07
Bilinen hata kalıpları, belirsiz ya da yeni alanError guessing, checklist, exploratory testing07

Tablo bir sıra değil, bir eşleme. Çoğu story birkaç satıra birden düşer. Kısmi sevkiyat story'sinde sayı aralıkları var (sevk edilen adet), koşul kombinasyonu var (kaçıncı sevkiyat ve siparişi tamamlıyor mu), geçmişe bağlı sonuç var (siparişin state'i), bir de zaman sınırı (30 gün). Bağımsız çok parametre yok: pairwise'a gerek yok. Kod henüz yazılırken branch coverage'a da gerek yok; o, testler yazıldıktan sonra eksik dal aramak için.

Story'yi Tekniklere Bölmek

Her kural cümlesini, ona en çok uyan tekniğin test condition'larına çeviriyoruz:

Birinci ve beşinci cümle siparişin yaşam döngüsünü tarif ediyor. Beşinci yazıdaki gibi state'leri ve event'leri çıkarıyoruz: Paid, PartiallyShipped, Shipped ve ClosedWithCancellation; event'ler Ship ve CancelRemaining, CancelRemaining'in guard'ı "sipariş tarihinden en az 30 gün sonra". Ship, Paid ve PartiallyShipped'den iki hedefe gidebiliyor: sevkiyat siparişi tamamlıyorsa Shipped'e, tamamlamıyorsa PartiallyShipped'e. CancelRemaining de aynı iki state'ten ClosedWithCancellation'a gidiyor. Toplam altı geçerli transition var; Shipped ve ClosedWithCancellation satırlarında dört geçersiz hücre kalıyor, guard'ın yanlış tarafı da ayrı bir test condition.

İkinci ve üçüncü cümle bir decision table: koşullar "kaçıncı sevkiyat" (1, 2, 3) ve "siparişi tamamlıyor mu"; aksiyonlar sevkiyatın kabul edilmesi, siparişin yeni state'i ve kargo ücreti. Altı kural, bir tanesi "reddedilir".

Sevk edilen adet için equivalence partitioning ve boundary value analysis: geçersiz partition'lar 0 ve altı, kalandan fazlası ve siparişte olmayan ISBN (International Standard Book Number; kitabın uluslararası kimlik numarası); sınırlar 0 ve 1, kalan ve kalan artı bir. 30 günlük guard için 30 günden bir tick önce ve tam 30. gün.

Dördüncü cümle tek bir test condition: fatura yalnızca sevk edilen kitapları içerir. Altıncı cümle ise yeni bir test istemiyor; en az 20 adet kuralı ilk yazıdan beri InstitutionalPricing'in testlerinde. Burada yalnızca 12 kitaplık bir ilk sevkiyatın kabul edildiğini görmek yeterli, o da zaten listede.

Error guessing listesinden iki madde ekleniyor: boş sevkiyat ve siparişin bir satırında kalan adedi aşan, diğer satırında geçerli olan bir sevkiyat. Toplamda teknikler 27 test condition üretti.

Tekrarları Atmak: Tek Test Listesi

27 condition'ı yan yana koyunca çoğunun aynı test olduğu görülüyor. Decision table'ın "ilk sevkiyat siparişi tamamlıyor" kuralı ile state transition'ın Paid'den Shipped'e geçişi aynı test. "İkinci sevkiyat tamamlıyor" kuralı, PartiallyShipped'den Shipped'e geçiş ve "kalan adedin tamamı" sınırı da aynı test. Birleştirince liste 19 test condition'a iniyor:

IDTestTeknik
T1İlk sevkiyat siparişin tamamı: Shipped, kargo ücreti alınırDecision table, state transition
T2İlk sevkiyat kısmi (12 Dune): PartiallyShipped, kargo ücreti alınırDecision table, state transition
T3Tek kitaplık sevkiyat kabul edilirBoundary value analysis (alt sınır)
T4İkinci sevkiyat kalanın tamamı: Shipped, kargo ücreti yokDecision table, state transition, boundary value analysis (üst sınır)
T5İkinci sevkiyat kısmi: PartiallyShipped kalırDecision table, state transition
T6Üçüncü sevkiyat siparişi tamamlar: ShippedDecision table
T7Üçüncü sevkiyat siparişi tamamlamıyor: reddedilirDecision table
T8–T9Adet 0 ve kalan artı bir: reddedilirEquivalence partitioning, boundary value analysis
T10Diğer satırda kalanı aşan adet: reddedilirError guessing
T11Siparişte olmayan ISBN: reddedilirEquivalence partitioning
T12Boş sevkiyat: reddedilirError guessing
T13Fatura yalnızca sevk edilen kitaplar içinEquivalence partitioning
T14–T15Kalanı iptal: 30 günden bir tick önce reddedilir, 30. günde iade edilirState transition (guard), boundary value analysis
T16–T19Shipped ve ClosedWithCancellation'da Ship ve CancelRemaining reddedilirState transition (geçersiz)

Teknik sütunu listenin en değerli kısmı. Bir test silinmek istendiğinde hangi coverage item'ın açıkta kalacağını söylüyor: T4'ü silersen hem decision table'ın bir kuralı hem bir transition hem bir sınır değeri testsiz kalır.

Listeden Koda

Liste doğrudan TheoryData'ya dönüşüyor. Decision table ile state transition'ın birleştiği altı satır tek bir Theory; satırların label'ı listedeki ID:

using Microsoft.Extensions.Time.Testing;

namespace Bookstore.Ordering.Tests;

public sealed class InstitutionalFulfillmentTests
{
    private const string Dune = "9780441013593";
    private const string Emma = "9780141439518";
    private static readonly Dictionary<string, string> Isbns = new() { ["Dune"] = Dune, ["Emma"] = Emma, ["Hobbit"] = "9780547928227" };
    private static readonly DateTimeOffset OrderedAt = new(2026, 9, 1, 9, 0, 0, TimeSpan.Zero);

    private readonly FakeTimeProvider _clock = new(OrderedAt);

    // Nevin Hanım'ın okulu: 12 Dune (150 TL) ve 10 Emma (95 TL), toplam 22 kitap; kargo 49,90 TL
    private InstitutionalFulfillment NewOrder() =>
        new([new(Dune, 12, 150m), new(Emma, 10, 95m)], 49.90m, OrderedAt, _clock);

    // "Dune:12 Emma:10" -> { Dune ISBN: 12, Emma ISBN: 10 }
    private static Dictionary<string, int> Items(string shipment) =>
        shipment.Split(' ', StringSplitOptions.RemoveEmptyEntries)
            .Select(item => item.Split(':'))
            .ToDictionary(parts => Isbns[parts[0]], parts => int.Parse(parts[1]));

    // T1-T6: decision table (kaçıncı sevkiyat x siparişi tamamlıyor mu) ve state transition'lar birlikte
    public static IEnumerable<TheoryDataRow<string[], FulfillmentState, decimal[]>> ShipmentRules =>
    [
        new(["Dune:12 Emma:10"], FulfillmentState.Shipped, [49.90m]) { Label = "T1 first shipment completes" },
        new(["Dune:12"], FulfillmentState.PartiallyShipped, [49.90m]) { Label = "T2 first shipment is partial" },
        new(["Dune:1"], FulfillmentState.PartiallyShipped, [49.90m]) { Label = "T3 one book (lower boundary)" },
        new(["Dune:12", "Emma:10"], FulfillmentState.Shipped, [49.90m, 0m]) { Label = "T4 second completes (exact remainder)" },
        new(["Dune:6", "Emma:10"], FulfillmentState.PartiallyShipped, [49.90m, 0m]) { Label = "T5 second is partial" },
        new(["Dune:6", "Dune:3", "Dune:3 Emma:10"], FulfillmentState.Shipped, [49.90m, 0m, 0m]) { Label = "T6 third completes" },
    ];

    [Theory]
    [MemberData(nameof(ShipmentRules))]
    public void Shipments_move_the_order_and_only_the_first_one_is_charged(
        string[] shipments, FulfillmentState expectedState, decimal[] expectedFees)
    {
        InstitutionalFulfillment order = NewOrder();

        foreach (string shipment in shipments)
        {
            order.Ship(Items(shipment));
        }

        Assert.Equal(expectedState, order.State);
        Assert.Equal(expectedFees, order.Shipments.Select(s => s.ShippingFee));
    }

    // T7: decision table'ın tek "hayır" kuralı
    [Fact]
    public void Third_shipment_that_does_not_complete_the_order_is_rejected()
    {
        InstitutionalFulfillment order = NewOrder();
        order.Ship(Items("Dune:6"));
        order.Ship(Items("Dune:3"));

        Assert.Throws<InvalidOperationException>(() => order.Ship(Items("Dune:3")));
        Assert.Equal(FulfillmentState.PartiallyShipped, order.State);
        Assert.Equal(2, order.Shipments.Count);
    }

    // T8-T12: equivalence partitioning ve boundary value analysis; her testte tek geçersiz değer
    [Theory]
    [InlineData("Dune:0")]
    [InlineData("Dune:13")]
    [InlineData("Emma:11")]
    [InlineData("Hobbit:1")]
    [InlineData("")]
    public void Invalid_shipment_is_rejected_and_nothing_is_shipped(string shipment)
    {
        InstitutionalFulfillment order = NewOrder();

        Assert.Throws<ArgumentException>(() => order.Ship(Items(shipment)));
        Assert.Equal(FulfillmentState.Paid, order.State);
        Assert.Empty(order.Shipments);
    }

    // T14-T15: guard [sipariş tarihinden en az 30 gün sonra]; sınır ve hemen öncesi
    [Fact]
    public void Remaining_books_cannot_be_cancelled_before_30_days()
    {
        InstitutionalFulfillment order = NewOrder();
        order.Ship(Items("Dune:12"));
        _clock.Advance(InstitutionalFulfillment.BackorderWindow - TimeSpan.FromTicks(1));

        Assert.Throws<InvalidOperationException>(() => order.CancelRemaining());
        Assert.Equal(FulfillmentState.PartiallyShipped, order.State);
    }

    [Fact]
    public void Remaining_books_are_cancelled_and_refunded_after_30_days()
    {
        InstitutionalFulfillment order = NewOrder();
        order.Ship(Items("Dune:12"));
        _clock.Advance(InstitutionalFulfillment.BackorderWindow);

        decimal refund = order.CancelRemaining();

        Assert.Equal(950m, refund);
        Assert.Equal(FulfillmentState.ClosedWithCancellation, order.State);
    }
}

T13 (fatura) ve T16–T19 (final state'lerdeki geçersiz geçişler) aynı sınıfta üç Fact daha; yazının sonundaki snapshot'ta tam hâli var. Items yardımcı method'u sevkiyatları tablodaki gibi okunur bir string'den kuruyor: "Dune:6", "Dune:3" ve "Dune:3 Emma:10" satırına bakınca üç sevkiyatı görmek, üç ayrı dictionary initializer okumaktan kolay. Eski koda karşı:

failed Bookstore.Ordering.Tests.InstitutionalFulfillmentTests.Shipments_move_the_order_and_only_the_first_one_is_charged [T6 third completes] (0ms)
  Xunit.MicrosoftTestingPlatform.XunitException: System.InvalidOperationException : Shipment 3 must complete the order.
failed Bookstore.Ordering.Tests.InstitutionalFulfillmentTests.Shipments_move_the_order_and_only_the_first_one_is_charged [T4 second completes (exact remainder)] (0ms)
  Assert.Equal() Failure: Values differ
  Expected: Shipped
  Actual:   PartiallyShipped

Test run summary: Failed!
  total: 17
  failed: 2
  succeeded: 15
  skipped: 0
  duration: 408ms

On yedi testten ikisi kırmızı ve ikisi de birden fazla tekniğin buluştuğu satırlar. Düzeltme, "tamamlıyor mu" sorusunu daha önce gönderilenleri de sayarak sormak:

bool completesOrder = lines.All(line => _shipped[line.Isbn] + items.GetValueOrDefault(line.Isbn) == line.Quantity);
Test run summary: Passed!
  total: 166
  failed: 0
  succeeded: 166
  skipped: 0
  duration: 465ms

Example Mapping, TDD Test List ve Test Seviyeleri

Bu liste yeni bir doküman değil. Requirements Analysis serisinin "Acceptance Criteria ve Example Mapping" yazısındaki example map'te her mavi kart bir kuraldı, her yeşil kart bir örnek. Test listesindeki her satır bir yeşil kart: T4, "12 Dune gönderildi, sonra 10 Emma; sipariş tamamlandı, ikinci sevkiyatta kargo ücreti yok" örneğinin ta kendisi. Teknikler, example mapping oturumunda sezgiyle bulunan örnekleri sistematik hâle getiriyor; oturumda kimsenin aklına gelmeyen "üçüncü sevkiyat siparişi tamamlıyor" örneği decision table'dan çıktı.

TDD (Test-Driven Development; testi koddan önce yazarak geliştirme) serisinin "TDD Nedir?" yazısındaki test list de aynı liste. Kent Beck'in Canon TDD'si işe test edilecek davranışların bir listesini yazarak başlar ve her döngüde listeden bir madde seçer. O listeyi tekniklerle kurmak, TDD'nin "sıradaki test hangisi?" sorusunu daha baştan cevaplıyor; TDD de listeyi kodun gerçekten ihtiyaç duyduğu sırayla tüketiyor.

Liste test seviyelerine de dağılıyor. On dokuz condition'ın hepsi unit seviyede; domain mantığı veritabanı ya da HTTP olmadan test edilebiliyor. Integration seviyesine iki şey kalıyor: faturanın ve sevkiyat kaydının veritabanına doğru yazıldığı ve 30. gündeki iadenin ödeme sağlayıcısına gerçekten gittiği. E2E (end-to-end; uygulamayı baştan sona çalıştıran test) seviyesine ise tek bir senaryo: Nevin Hanım kısmi sevkiyatı seçer, ilk koli gider, stok gelince ikincisi gider, siparişler sayfasında sipariş tamamlanmış görünür. Bu senaryonun Gherkin hâli Requirements Analysis serisinin Gherkin yazısındaki gibi yazılır; on dokuz condition'ı E2E'ye taşımak, unit seviyede milisaniyeler süren testleri dakikalara çevirir.

AI'dan Test Case İstemek

Bir story'yi bir LLM'e verip "test case'leri yaz" demek artık sık yapılan bir şey. ISTQB'nin (International Software Testing Qualifications Board; yazılım test sertifikalarını tanımlayan uluslararası kurul) 2025'te yayımladığı Testing with Generative AI syllabus'ı bu işi ayrı bir sertifika konusu yapıyor. Syllabus'ın önerdiği prompt yapısı altı parçadan oluşuyor: rol, bağlam, talimat, girdi verisi, kısıtlar ve çıktı formatı. Aynı syllabus LLM çıktısındaki hallucination, akıl yürütme hatası ve bias'ları tanımayı ve azaltmayı ayrı bir bölüm olarak işliyor; sonuçları değerlendirme sorumluluğu test edende kalıyor.

Bu serinin teknikleri prompt'un talimat ve kısıt kısmını dolduruyor. "Test case'leri yaz" yerine tekniği ve coverage hedefini açıkça istemek, çıktıyı review edilebilir hâle getiriyor:

Role: You are a test designer for an online bookstore's back office.
Context: Institutional orders can be shipped in parts when stock is short.
Instruction: Derive test cases for the rules below with these techniques:
- equivalence partitioning: list every valid and invalid partition; one invalid value per test
- boundary value analysis: test both sides of every boundary (2-value)
- decision table: list every combination of "shipment number" and "completes the order"
- state transition: build the state table, cover every valid transition and every invalid cell
Input data: <the six rules of the story>
Constraints: Don't invent rules that aren't in the input. If a rule is ambiguous, list it as a question instead of guessing an expected result.
Output format: a table with columns ID, technique, coverage item, input, expected result.

Çıktıyı kendi listenle karşılaştırırken teknik ve coverage item sütunları işe yarıyor; hangi item'ın eksik, hangisinin fazladan olduğunu satır satır görebiliyorsun. Review checklist'i bu serinin tekniklerinden çıkıyor:

  • Story'nin her kuralı en az bir teste bağlı mı? Story'de olmayan bir kural test edilmiş mi?
  • Her geçersiz partition ayrı bir testte mi, yoksa iki geçersiz değer aynı testte mi?
  • Her sınırın iki yanı var mı: 0 ve 1, kalan ve kalan artı bir, 30 günden önce ve 30. gün?
  • State table'ın boş hücreleri test edilmiş mi, yalnızca geçerli transition'lar mı var?
  • Beklenen sonuçlar story'den mi geliyor, yoksa makul görünen ama story'de olmayan değerler mi?
  • Aynı coverage item'ı iki kez test eden satırlar var mı?
  • Belirsiz bir kural için tahmin yerine soru üretilmiş mi?

Beşinci madde en kritik olanı. "Üçüncü sevkiyat siparişi tamamlamıyorsa ne olur?" sorusuna story'de cevap olmasaydı, akla yatkın bir beklenen sonuç uydurmak kolaydır ve bu, hatayı teste kopyalamanın en hızlı yolu. AI'ın ürettiği test listesi bir taslak; tekniklerle kurulan liste ise onu doğrulamanın ölçüsü.

Üretimde Dikkat

Test listesini story ile birlikte sakla. Pull request'te listenin ID'lerini test adlarında ya da label'larda görmek, review eden kişinin "bu kural test edilmiş mi?" sorusunu kodu okumadan cevaplamasını sağlıyor.

Listeyi kural değiştikçe yeniden kur. Selin yarın sevkiyat sınırını dörde çıkarırsa decision table'a yeni sütunlar, state table'a yeni bir yol gelir. Eski listeye bir satır eklemek, yeni kuralın diğerleriyle kesişimlerini atlamak demek; dördüncü yazıdaki gibi önce tam tabloyu, sonra sadeleştirmeyi yeniden yap.

Coverage'ı en sonda bir kontrol olarak kullan. Liste requirement'tan kurulduktan ve testler geçtikten sonra branch coverage raporu, story'nin söylemediği ama kodun yaptığı bir şey olup olmadığını gösterir. Rapor yüzde 100 değilse eksik dal ya story'ye yazılmamış bir kuraldır ya da ölü koddur; ikisi de konuşulması gereken şeyler.

Production'dan gelen hatayı listeye geri yaz. Bir hata testlerin yakalamadığı bir yerden geldiyse, listeye eklenen test kadar "hangi teknik bunu bulurdu?" sorusunun cevabı da önemli. Cevap error guessing listesine ya da checklist'e yeni bir madde olarak döner.

Sık Düşülen Tuzaklar

Her tekniği her story'ye uygulamak. Kısmi sevkiyat story'sinde pairwise'ın yapacağı bir iş yok; parametreleri az ve birbirine bağlı. Yedi tekniği her story için çalıştırmak, aynı test'in yedi farklı adla yazılması ve haftalarca süren test tasarımı demek. Requirement'ın şekline bakıp iki üç teknik seçmek yeter.

Test case'leri dokümanda bırakmak. Tablo bir Confluence sayfasında kalırsa ilk değişiklikte koddan kopar. Liste ancak TheoryData satırlarına ve test adlarına dönüştüğünde gerçekten çalışan bir şey olur.

Tekrarları atmamak. 27 condition'ı 27 ayrı test olarak yazmak, aynı davranışı üç kez test eden ve bir değişiklikte üçü birden kırmızıya dönen bir test seti üretir. Birleştirilmiş liste daha az test, daha net bir hata mesajı demek.

AI'ın listesini review'suz kabul etmek. Akıcı ve eksiksiz görünen bir tablo, story'de olmayan beklenen sonuçlar içerebilir. Review checklist'i her satır için uygulanmadan liste koda girmemeli.

Test sayısını kaliteyle karıştırmak. Serinin ilk yazısında gördüğümüz gibi ölçü test sayısı değil, kapsanan coverage item'lar. On yedi test, beş farklı teknikten gelen on dokuz condition'ı kapsıyor.

Küçük Bir Deneme

★☆☆ T4'ü listeden ve koddan sil. Hangi coverage item'lar testsiz kalıyor? Eski koddaki hatayı başka hangi test yakalıyordu?

★★☆ Story'ye bir kural ekle: "Bir kampüse giden kitaplar aynı sevkiyatta gider." Hangi teknik(ler) yeni test condition üretiyor? Listeyi güncelle, tekrarları at ve yeni testleri yaz.

★★★ Bu yazıdaki prompt'u kullandığın bir LLM ile çalıştır. Çıktıyı review checklist'iyle değerlendir: kaç satır listedeki bir ID ile eşleşiyor, kaçı eksik, kaçı story'de olmayan bir şeyi test ediyor? Bulduğun eksikleri prompt'un kısıt kısmına ekleyip tekrar dene.

Seri burada bitiyor. Sekiz yazıda test case'leri sezgiyle seçmekten sistematik olarak türetmeye geçtik: requirement'tan test condition'a, oradan coverage item'a ve en sonunda çalışan bir xUnit testine. Teknikler test yazmayı yavaşlatmıyor, neyin yazılacağını ve neyin yazılmayacağını önceden söyledikleri için hızlandırıyor. Bir sonraki story geldiğinde ilk soru "kaç test yazayım?" değil, "bu requirement hangi şekilde ve hangi teknik ona uyuyor?" olsun.