State Transition Testing: Sipariş State Machine'inden Test Case Üretmek, Geçersiz Geçişler ve 0-switch/1-switch Coverage
State transition testing, sistemin davranışı geçmişine bağlıysa test case'leri state diagram'ından ve state table'dan çıkarır: geçerli transition'lar (0-switch), state table'ın boş hücrelerindeki geçersiz transition'lar ve art arda iki transition'dan oluşan çiftler (1-switch). Bookstore'un siparişinde üç yeşil test, kargodaki siparişin iptal edilebilmesini ve beklemeden dönen siparişin kargoya verilememesini kaçırdı; 40 geçersiz geçiş ve 10 transition çifti ikisini de yakaladı.
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çı) siparişin yaşam döngüsü bir state machine. Sipariş Created olarak doğar, ödenince Paid olur, kargoya verilince Shipped, müşteriye ulaşınca Delivered. Ödenmiş ama henüz kargoya verilmemiş sipariş iptal edilebilir ve para iade edilir. Teslimden sonraki 14 gün içinde müşteri kitabı iade edebilir. Bir de depo durumu var: sayımda kitabın rafta olmadığı anlaşılırsa sipariş OnHold'a alınır ve stok rezervasyonu düşer; tedarikçiden kitap gelince Release ile yeniden Paid olur.
Ayşe kodu tek bir Fire method'u ve tuple üzerinde bir switch ile yazmış:
namespace Bookstore.Ordering;
public enum OrderState
{
Created,
Paid,
OnHold,
Shipped,
Delivered,
Cancelled,
Returned,
}
public enum OrderEvent
{
Pay,
Hold,
Release,
Ship,
Deliver,
Cancel,
Return,
}
public sealed class Order(TimeProvider clock)
{
public static readonly TimeSpan ReturnWindow = TimeSpan.FromDays(14);
private readonly List<string> _actions = [];
private bool _stockReserved;
private DateTimeOffset? _deliveredAt;
public OrderState State { get; private set; } = OrderState.Created;
public IReadOnlyList<string> Actions => _actions;
public void Fire(OrderEvent e)
{
switch (e, State)
{
case (OrderEvent.Pay, OrderState.Created):
_stockReserved = true;
State = OrderState.Paid;
break;
case (OrderEvent.Hold, OrderState.Paid):
_stockReserved = false;
State = OrderState.OnHold;
break;
case (OrderEvent.Release, OrderState.OnHold):
State = OrderState.Paid;
break;
case (OrderEvent.Ship, OrderState.Paid):
if (!_stockReserved)
{
throw new InvalidOperationException("Cannot ship an order without a stock reservation.");
}
_stockReserved = false;
State = OrderState.Shipped;
break;
case (OrderEvent.Deliver, OrderState.Shipped):
_deliveredAt = clock.GetUtcNow();
State = OrderState.Delivered;
break;
case (OrderEvent.Cancel, not (OrderState.Delivered or OrderState.Cancelled or OrderState.Returned)):
if (State != OrderState.Created)
{
_actions.Add("Refund");
}
_stockReserved = false;
State = OrderState.Cancelled;
break;
case (OrderEvent.Return, OrderState.Delivered) when clock.GetUtcNow() - _deliveredAt <= ReturnWindow:
_actions.Add("Refund");
State = OrderState.Returned;
break;
default:
throw new InvalidOperationException($"{e} is not allowed when the order is {State}.");
}
}
}
Ekibin testleri siparişin normal akışını, ödenmiş siparişin iptalini ve beklemeden dönüşü deniyor:
// Ekibin mevcut testleri: normal akış, bir iptal ve bekletme
[Fact]
public void Order_goes_from_created_to_delivered()
{
Order order = new(_clock);
order.Fire(OrderEvent.Pay);
order.Fire(OrderEvent.Ship);
order.Fire(OrderEvent.Deliver);
Assert.Equal(OrderState.Delivered, order.State);
}
[Fact]
public void Paid_order_can_be_cancelled_with_a_refund()
{
Order order = new(_clock);
order.Fire(OrderEvent.Pay);
order.Fire(OrderEvent.Cancel);
Assert.Equal(OrderState.Cancelled, order.State);
Assert.Equal(["Refund"], order.Actions);
}
[Fact]
public void Held_order_is_released_back_to_paid()
{
Order order = new(_clock);
order.Fire(OrderEvent.Pay);
order.Fire(OrderEvent.Hold);
order.Fire(OrderEvent.Release);
Assert.Equal(OrderState.Paid, order.State);
}
Test run summary: Passed!
total: 3
failed: 0
succeeded: 3
skipped: 0
duration: 281ms
Üç test de yeşil. İki soru: kargoya verilmiş bir siparişi iptal etmeye çalışırsan ne olur? Beklemeden dönen bir sipariş kargoya verilebilir mi?
Cevap: Kodda İki Hata, Testlerde Hiç Kırmızı Yok
Kargodaki sipariş iptal ediliyor. Cancel kolu izin verilen state'leri saymak yerine yasak olanları sayıyor ve listeye Shipped'ı koymayı unutuyor. Sonuç: kitap yolda, para iade edilmiş, sipariş Cancelled. Bu tür bir hata log'da ya da muhasebede, haftalar sonra fark edilir.
Beklemeden dönen sipariş ise kargoya verilemiyor. Release state'i Paid yapıyor ama Hold'un düşürdüğü stok rezervasyonunu geri almıyor; bir sonraki Ship exception fırlatıyor. Depodaki çalışan "bu siparişi gönder" dediği anda hata alıyor.
Mevcut testler bu iki hatanın yanından geçiyor, çünkü yalnızca yazılması gereken akışları deniyor. State machine'de hatalar genellikle iki yerde durur: hiç izin verilmemesi gereken geçişlerde ve geçişlerin belli bir sırayla arka arkaya geldiği yerde. Bu yazının tekniği, state transition testing, tam bu iki yeri sistematik olarak test etmek için var.
State, Event, Guard, Action
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'ı state transition testing'i bir state diagram üzerinden anlatır. Diagram sistemin state'lerini ve aralarındaki geçerli transition'ları gösterir. Bir transition'ı bir event başlatır; event'in yanında bir guard koşulu olabilir ve transition sırasında sistem bir action çalıştırabilir. Okları etiketlemenin yaygın biçimi event [guard] / action. Bizim siparişte "Return [≤ 14 gün] / Refund" bu üçünü birden taşıyor: event Return, guard teslimden bu yana geçen süre, action para iadesi.
Diagram'ı UML (Unified Modeling Language; yazılım tasarımında kullanılan standart diyagram dili) serisinin "State Machine Diagram" yazısındaki gibi çiziyoruz; o yazı siparişin yaşam döngüsünü diagram'dan koda taşıyordu, biz aynı diagram'dan test case çıkaracağız:

Diagram'da yedi state ve dokuz geçerli transition var. Ama diagram yalnızca izin verilenleri gösteriyor. Syllabus burada ikinci bir modele, state table'a geçer: satırlar state, sütunlar event, hücreler hedef state. Diagram'la aynı bilgiyi taşır, bir farkla: boş hücreler geçersiz transition'ları açıkça gösterir.
| State | Pay | Hold | Release | Ship | Deliver | Cancel | Return |
|---|---|---|---|---|---|---|---|
| Created | Paid | Cancelled | |||||
| Paid | OnHold | Shipped | Cancelled / Refund | ||||
| OnHold | Paid | Cancelled / Refund | |||||
| Shipped | Delivered | ||||||
| Delivered | [≤ 14 gün] Returned / Refund | ||||||
| Cancelled | |||||||
| Returned |
Kırk dokuz hücreden dokuzu dolu, kırkı boş. Shipped satırının Cancel sütunu boş: kargodaki sipariş iptal edilemez. Requirement bunu "henüz kargoya verilmemiş sipariş iptal edilebilir" diye söylüyordu; tablo aynı şeyi bir hücre olarak, test edilebilir hâlde söylüyor. Cancelled ve Returned satırları tamamen boş; bunlar final state'ler.
Üç Coverage Kriteri
Syllabus state transition testing için üç coverage kriteri tanımlar ve aralarındaki ilişki test sayısını doğrudan belirler.
All states coverage'da coverage item'lar state'lerdir: her state en az bir kez ziyaret edilmeli. Bizim diagram'da iki test case yeter: Pay, Hold, Release, Ship, Deliver, Return dizisi Cancelled dışındaki altı state'i gezer, tek bir Cancel da yedincisini. Bu kriter zayıftır; dokuz transition'dan üçünü hiç çalıştırmadan yüzde 100'e ulaşır.
Valid transitions coverage'da, diğer adıyla 0-switch coverage'da, coverage item'lar tek tek geçerli transition'lardır. Dokuz transition'ın hepsi en az bir kez çalışmalı. Syllabus'a göre en yaygın kullanılan kriter bu. Uzun dizilerle dört test case'te tamamlanabilir; biz her transition'ı ayrı bir test olarak yazacağız, çünkü kırmızı bir testin hangi transition'a ait olduğunu tek bakışta görmek istiyoruz.
All transitions coverage'da coverage item'lar state table'daki bütün hücrelerdir: dokuz geçerli transition çalıştırılmalı, kırk geçersiz transition da denenmeli. Syllabus her test case'te yalnızca bir geçersiz transition denemeyi önerir; ikinci yazıda equivalence partitioning'de gördüğümüz maskeleme burada da geçerli. Syllabus ayrıca bu kriteri görev ve güvenlik açısından kritik yazılım için asgari şart olarak görür. Para iadesi yapan bir sipariş akışı da bu sınıfa yakın duruyor.
State Table'dan TheoryData'ya
Testlerin kaynağı kod değil, diagram. Önceki yazıda testi koddaki kural tablosundan kopyalamanın aynı hatayı iki kez yazmak olduğunu görmüştük; burada da modeli test projesinde, diagram'dan bağımsız olarak yazıyoruz:
namespace Bookstore.Ordering.Tests;
// UML state machine diagram'ından çıkarılan model; koddan değil, diagram'dan
public static class OrderStateModel
{
public sealed record Transition(OrderState From, OrderEvent Event, OrderState To, bool Refunds = false);
// Geçerli geçişler: event [guard] / action
public static readonly Transition[] Valid =
[
new(OrderState.Created, OrderEvent.Pay, OrderState.Paid),
new(OrderState.Created, OrderEvent.Cancel, OrderState.Cancelled),
new(OrderState.Paid, OrderEvent.Hold, OrderState.OnHold),
new(OrderState.Paid, OrderEvent.Ship, OrderState.Shipped),
new(OrderState.Paid, OrderEvent.Cancel, OrderState.Cancelled, Refunds: true),
new(OrderState.OnHold, OrderEvent.Release, OrderState.Paid),
new(OrderState.OnHold, OrderEvent.Cancel, OrderState.Cancelled, Refunds: true),
new(OrderState.Shipped, OrderEvent.Deliver, OrderState.Delivered),
new(OrderState.Delivered, OrderEvent.Return, OrderState.Returned, Refunds: true), // [teslimden sonra 14 gün içinde]
];
// Her state'e Created'dan giden en kısa event dizisi
public static readonly Dictionary<OrderState, OrderEvent[]> ShortestPath = new()
{
[OrderState.Created] = [],
[OrderState.Paid] = [OrderEvent.Pay],
[OrderState.OnHold] = [OrderEvent.Pay, OrderEvent.Hold],
[OrderState.Shipped] = [OrderEvent.Pay, OrderEvent.Ship],
[OrderState.Delivered] = [OrderEvent.Pay, OrderEvent.Ship, OrderEvent.Deliver],
[OrderState.Cancelled] = [OrderEvent.Cancel],
[OrderState.Returned] = [OrderEvent.Pay, OrderEvent.Ship, OrderEvent.Deliver, OrderEvent.Return],
};
}
ShortestPath önemli bir karar: bir siparişi Shipped'a getirmek için State property'sine değer atamıyoruz, gerçek event'leri sırayla çalıştırıyoruz. Test, siparişin o state'e üretimdeki gibi geldiğini varsayıyor. Testler bu iki tablodan üretiliyor:
using Microsoft.Extensions.Time.Testing;
using static Bookstore.Ordering.Tests.OrderStateModel;
namespace Bookstore.Ordering.Tests;
public sealed class OrderStateTransitionTests
{
private readonly FakeTimeProvider _clock = new(new DateTimeOffset(2026, 11, 2, 9, 0, 0, TimeSpan.Zero));
private Order OrderIn(OrderState state)
{
Order order = new(_clock);
foreach (OrderEvent e in ShortestPath[state])
{
order.Fire(e);
}
return order;
}
// 0-switch (valid transitions) coverage: her geçerli geçiş bir kez
public static TheoryData<OrderState, OrderEvent, OrderState, bool> ValidTransitions =>
new(Valid.Select(t => (t.From, t.Event, t.To, t.Refunds)));
[Theory]
[MemberData(nameof(ValidTransitions))]
public void Valid_transition_moves_the_order_to_the_target_state(
OrderState from, OrderEvent e, OrderState to, bool refunds)
{
Order order = OrderIn(from);
int refundsBefore = order.Actions.Count(a => a == "Refund");
order.Fire(e);
Assert.Equal(to, order.State);
Assert.Equal(refunds ? 1 : 0, order.Actions.Count(a => a == "Refund") - refundsBefore);
}
// State table'daki boş hücreler: 7 state x 7 event - 9 geçerli geçiş = 40 geçersiz geçiş
public static TheoryData<OrderState, OrderEvent> InvalidTransitions =>
new(from state in Enum.GetValues<OrderState>()
from e in Enum.GetValues<OrderEvent>()
where !Valid.Any(t => t.From == state && t.Event == e)
select (state, e));
[Theory]
[MemberData(nameof(InvalidTransitions))]
public void Invalid_transition_is_rejected_and_the_state_does_not_change(OrderState from, OrderEvent e)
{
Order order = OrderIn(from);
Assert.Throws<InvalidOperationException>(() => order.Fire(e));
Assert.Equal(from, order.State);
}
}
Geçersiz transition'ları tek tek yazmıyoruz; state table'ın boş hücrelerini, yani bütün state ve event çarpımından geçerli olanları çıkararak üretiyoruz. Böylece diagram'a yeni bir transition eklendiğinde geçersiz listesi kendiliğinden küçülür, yeni bir state ya da event eklendiğinde kendiliğinden büyür. Geçersiz bir transition'ın iki iddiası var: exception fırlatılır ve state değişmez. İkincisi en az birincisi kadar önemli; exception'dan önce state'i değiştiren bir kod, hata mesajı verip siparişi yine de bozmuş olur. TheoryData mekaniği Unit Test serisinin "xUnit v3'te Veri Odaklı Testler" yazısında anlatılıyor. Eski koda karşı:
failed Bookstore.Ordering.Tests.OrderStateTransitionTests.Invalid_transition_is_rejected_and_the_state_does_not_change(from: Shipped, e: Cancel) (0ms)
Assert.Throws() Failure: No exception was thrown
Expected: typeof(System.InvalidOperationException)
Test run summary: Failed!
total: 108
failed: 1
succeeded: 107
skipped: 0
duration: 252ms
Kırk geçersiz transition'dan biri kırmızı: kargodaki siparişin iptali. Dokuz geçerli transition'ın hepsi ise yeşil; 0-switch coverage yüzde 100 ve ikinci hata hâlâ görünmüyor.
1-switch Coverage: Kodda Diagram'dan Fazla State Var
0-switch testleri her transition'ı bir kez çalıştırdı, ama her birini siparişin o state'e en kısa yoldan geldiği bir durumda. Paid'e her seferinde Pay ile geldik, hiç Release ile gelmedik. Hata ise tam orada: kodda iki farklı Paid var. Pay'den gelen Paid'de stok rezerve, Release'ten gelen Paid'de değil. Diagram'da tek bir state olan şey, kodda State ve _stockReserved alanlarının iki farklı kombinasyonu.
Bu tür hataları bulmak için bir adım ileri gidip transition çiftlerine bakıyoruz. N-switch coverage fikri Tsun S. Chow'un 1978'de IEEE (Institute of Electrical and Electronics Engineers; elektrik ve elektronik mühendisleri derneği) Transactions on Software Engineering dergisinde yayımlanan, finite-state machine'lerle modellenmiş tasarımların test edilmesi üzerine makalesine dayanır: 0-switch tek transition'lar, 1-switch art arda iki transition'dan oluşan diziler, N-switch de N+1 transition'lık dizilerdir. ISTQB glossary'si terimi bu tanımla verir; CTFL v4.0.1 syllabus'ı ise Foundation seviyesinde yalnızca yukarıdaki üç kriteri anlatır. 1-switch'in değeri, bir transition'ın davranışının siparişin o state'e hangi transition'la geldiğine bağlı olup olmadığını sorması. Doğru kodda bağlı olmamalı.
Çiftleri de modelden üretiyoruz: ilk transition'ın hedef state'inden çıkan her transition bir çift oluşturur. Dokuz transition'dan on çift çıkıyor:
// 1-switch coverage: art arda iki geçerli geçişin bütün çiftleri
public static IEnumerable<TheoryDataRow<OrderEvent[], OrderState>> TransitionPairs =>
from first in Valid
from second in Valid
where second.From == first.To
select new TheoryDataRow<OrderEvent[], OrderState>(
[.. ShortestPath[first.From], first.Event, second.Event], second.To)
{
Label = $"{first.Event} -> {second.Event}",
};
[Theory]
[MemberData(nameof(TransitionPairs))]
public void Pair_of_transitions_ends_in_the_expected_state(OrderEvent[] events, OrderState expected)
{
Order order = new(_clock);
foreach (OrderEvent e in events)
{
order.Fire(e);
}
Assert.Equal(expected, order.State);
}
Yalnızca Ordering test projesini çalıştırıyoruz (dotnet test --project tests/Bookstore.Ordering.Tests):
failed Bookstore.Ordering.Tests.OrderStateTransitionTests.Pair_of_transitions_ends_in_the_expected_state [Release -> Ship] (0ms)
Xunit.MicrosoftTestingPlatform.XunitException: System.InvalidOperationException : Cannot ship an order without a stock reservation.
failed Bookstore.Ordering.Tests.OrderStateTransitionTests.Invalid_transition_is_rejected_and_the_state_does_not_change(from: Shipped, e: Cancel) (0ms)
Assert.Throws() Failure: No exception was thrown
Expected: typeof(System.InvalidOperationException)
Test run summary: Failed!
total: 91
failed: 2
succeeded: 89
skipped: 0
duration: 304ms
On çiftin yedisi 0-switch testlerinin zaten çalıştırdığı dizilerdi, çünkü ShortestPath önekleri onları kendiliğinden içeriyor. Yeni olan üçü Release ile başlayanlar, kırmızı olan da Release ve Ship çifti. Bir not: 0-switch coverage'ı dört uzun test case'le tamamlamayı seçseydik Pay, Hold, Release, Ship dizisi bu hatayı da yakalayabilirdi. Kriter bunu garanti etmez; garanti 1-switch'te. Düzeltme iki satır:
case (OrderEvent.Release, OrderState.OnHold):
_stockReserved = true;
State = OrderState.Paid;
break;
case (OrderEvent.Cancel, OrderState.Created or OrderState.Paid or OrderState.OnHold):
İkinci satır Cancel kolunu yasaklıları saymaktan izin verilenleri saymaya çeviriyor. Yarın diagram'a yeni bir state eklendiğinde (örneğin kargo firmasından dönen ReturnedToSender) eski kalıp onu sessizce iptal edilebilir sayardı; yeni kalıp saymaz.
Guard Koşulunu Sınırıyla Test Etmek
Return transition'ının guard'ı bir sınır: teslimden sonra en fazla 14 gün. Guard'ı olan bir transition'ın aslında iki çıktısı var, guard doğruysa Returned, yanlışsa geçersiz transition. Tek bir testle "Return çalışıyor" demek guard'ın yarısını test etmek olur. Üçüncü yazıdaki boundary value analysis'i doğrudan uyguluyoruz; saati FakeTimeProvider ile ilerletiyoruz:
// Guard: Return [teslimden sonra en fazla 14 gün]; sınır ve hemen sonrası
[Fact]
public void Return_is_allowed_exactly_14_days_after_delivery()
{
Order order = OrderIn(OrderState.Delivered);
_clock.Advance(Order.ReturnWindow);
order.Fire(OrderEvent.Return);
Assert.Equal(OrderState.Returned, order.State);
}
[Fact]
public void Return_is_rejected_one_tick_after_the_window()
{
Order order = OrderIn(OrderState.Delivered);
_clock.Advance(Order.ReturnWindow + TimeSpan.FromTicks(1));
Assert.Throws<InvalidOperationException>(() => order.Fire(OrderEvent.Return));
Assert.Equal(OrderState.Delivered, order.State);
}
Bu iki test eski kodda da geçiyordu; guard doğru yazılmıştı. Ama bunu ancak sınırın iki yanını deneyince biliyoruz. Zamanı test etmenin mekaniği Unit Test serisinin "Unit Test'te Zor Durumlar" yazısında; burada önemli olan, guard'ın bir partition sınırı olduğunu görmek. Bütün düzeltmelerden sonra:
Test run summary: Passed!
total: 120
failed: 0
succeeded: 120
skipped: 0
duration: 269ms
Diagram'dan Koda, Koddan E2E'ye
Bu testler Order'ın içini bilmiyor; yalnızca Fire ve State'i kullanıyor. Design Patterns serisinin "State Pattern" yazısındaki gibi her state'i ayrı bir object'e taşıyan bir implementasyona geçsen test projesinde tek satır değişmez. State transition testing bir black-box teknik; modeli test eder, implementasyonu değil.
Aynı model test seviyelerine de dağılıyor. Elli dokuz unit test (9 + 40 + 10) milisaniyeler içinde koşuyor ve state machine'in bütün hücrelerini kapsıyor. Uçtan uca seviyede ise E2E (end-to-end; kullanıcının gördüğü uygulamayı baştan sona çalıştıran test) için genellikle tek bir senaryo yeter: Pay, Ship, Deliver dizisinin API, veritabanı ve arayüz üzerinden gerçekten çalıştığını gösteren senaryo. Kırk geçersiz transition'ı tarayıcıda denemek hem yavaş hem de gereksiz; onların kanıtı unit seviyede. Bu senaryonun nasıl yazılacağı Integration ve E2E Test serisinin konusu.
Kurumsal siparişlerdeki satın alma onayı da aynı tekniğe uygun bir state machine: PurchaseApproval Pending olarak başlar, onaylayan kişi tutarı kendi limitinin altındaysa Approved, üstündeyse ikinci onaya düşer, reddederse Rejected olur. Burada guard tutar limiti ve onaylayanın siparişi oluşturan kişiden farklı olması; ikisi de sınır değerleriyle test edilir. Yazının sonundaki alıştırmalardan biri bu.
Üretimde Dikkat
State table'ı requirement'ın yanında tut. Boş hücreler en çok tartışma çıkaran yer: "teslim edilmiş sipariş iptal edilebilir mi?" sorusu ancak tablo önüne konunca sorulur. Her boş hücrenin bilinçli olarak boş olduğunu Selin'le birlikte onaylamak, bir hafta sonra müşteri hizmetlerinden gelecek "neden iptal edemiyoruz?" sorusunu önceden cevaplar.
Geçersiz transition'ı log'la. Üretimde InvalidOperationException sessizce yutuluyorsa, kargodaki siparişi iptal etmeye çalışan bir ekran ya da çift tıklanan bir buton hiç görünmez. Hangi state'te hangi event'in reddedildiğini log'a yazmak, test edilmemiş bir akışı ilk fark ettiren şey olabilir.
State'i ve gizli alanları birlikte sakla. Bu yazıdaki hata _stockReserved alanının state'ten ayrı yaşamasından çıktı. Kalıcı bir sistemde bu alan veritabanında da ayrı bir kolon olur ve state ile tutarsız hâle gelebilir. Böyle alanlar varsa 1-switch testleri ve "her state'te bu alan şu değerde olmalı" gibi bir invariant testi birlikte düşünülmeli.
Event'ler her zaman sırayla gelmez. Ödeme sağlayıcısının webhook'u, kargo firmasının teslim bildirimi, depo ekranındaki iki tıklama aynı siparişe neredeyse aynı anda gelebilir. State machine'in kendisi tek thread'de doğru olsa bile, eşzamanlı event'ler için ayrı bir koruma (optimistic concurrency ya da tek bir consumer) gerekir. Bu, unit seviyede değil, integration seviyesinde test edilir.
Sık Düşülen Tuzaklar
Yalnızca normal akışı test etmek. Yazının başındaki üç test bunun örneği: hepsi doğru, hepsi yeşil ve iki hatanın ikisini de göremedi. Bir state machine'in testlerinin çoğu geçersiz transition'larda olmalı; bu siparişte geçerli dokuz transition'a karşılık kırk geçersiz transition var.
Guard'ı olan bir transition'ı tek testle geçmek. Guard bir koşul, koşulun iki tarafı var. "14 gün içinde iade edilebilir" diyen bir test ancak 14. gün ve hemen sonrası denendiğinde guard'ı test etmiş olur.
Testte state'i doğrudan atamak. order.State = OrderState.Paid yazabilseydik testler daha kısa olurdu, ama Release'ten gelen Paid'i hiç üretemezdik. State'e gerçek event'lerle gelmek, kodun gizli alanlarını da üretimdeki gibi kurar.
Modeli koddan çıkarmak. Order.Fire'daki case kollarını okuyup state table'ı doldurursan, kargodaki siparişin iptali de "geçerli" bir hücre olarak tabloya girer. Tablo requirement'tan ve diagram'dan kurulur.
Her state machine'de 1-switch'e gitmek. On çift bu diagram için ucuz; otuz state ve yüz transition'lık bir iş akışında çift sayısı hızla büyür. 1-switch'i döngülerin ve gizli state şüphesi olan yerlerin etrafında kullanmak, her yere uygulamaktan daha verimli.
Küçük Bir Deneme
★☆☆ Cancel kolunu eski hâline (not (...)) geri al ve listeye Shipped'ı ekle. Hangi test yeşile döner? Sonra diagram'a ReturnedToSender adlı yeni bir final state ekle ve Enum'a da ekle. Eski kalıp bu yeni state'te iptale izin verir mi? Hangi test bunu yakalar?
★★☆ Bir invariant testi yaz: her state'e en kısa yoldan ve Release üzerinden gidip, Paid'de stok rezervasyonunun olduğunu doğrula. _stockReserved private; test için hangi public davranışı kullanırsın? Bu testin 1-switch testinden farkı ne?
★★★ PurchaseApproval için state diagram'ı ve state table'ı kur: Pending, AwaitingSecondApproval, Approved, Rejected; event'ler Approve, Reject; guard'lar onaylayanın limiti ve onaylayanın siparişi oluşturan kişiden farklı olması. Kaç geçersiz transition var? Guard'ları sınır değerleriyle test eden TheoryData'yı yaz.
Bu yazıda siparişin geçmişi sonucu değiştiriyordu. Bir sonraki yazıda sonucun birbirinden bağımsız görünen birçok parametrenin kombinasyonuna bağlı olduğu yere geçiyoruz: tarayıcı, dil, müşteri tipi, ödeme yöntemi ve teslimat tipi. Bütün kombinasyonları denemek yüzlerce test demek; pairwise testing ve Microsoft'un PICT (Pairwise Independent Combinatorial Testing; kombinasyon üreten komut satırı aracı) aracıyla bunu bir düzine teste indirecek ve iki parametrenin etkileşiminde saklanan bir hatayı yakalayacağız.