Gherkin ile Executable Specification: Given-When-Then, Rule, Scenario Outline ve Declarative Senaryo Yazmak
Gherkin, Example Mapping'de bulunan kural ve örnekleri hem stakeholder'ın okuyabileceği hem de test runner'ın çalıştırabileceği bir spesifikasyona çevirir; iyi senaryo arayüzü değil, iş kuralını anlatır. Bookstore'un kurumsal sipariş kurallarıyla Feature, Rule, Background, Scenario Outline, Examples ve data table'ı, declarative ile imperative senaryo farkını, Türkçe keyword kararını ve Gherkin anti-pattern'lerini anlatıyoruz.
Sürüm notu: Bu yazıdaki Gherkin dosyaları Cucumber'ın Gherkin referansına göre yazılmış; .NET 10, Reqnroll 3.3.4 ve xUnit v3 4.0.3 ile çalıştırılmıştır.
Önce Tahmin Et
Emre, 05. yazıdaki "Kurumsal indirim" story'si için ilk senaryoyu yazmış ve review'a açmış:
Scenario: Discount on a school order
Given I open "https://localhost:5001/login"
And I log in as "purchasing@school.example"
And I click "New institutional order"
And I paste "9786050000016;40" into the "Lines" text area
And I click "Check"
When I click "Calculate total"
Then I should see "9,000.00" in the "#order-total" element
Senaryo çalışıyor ve yeşil. Üç soru. Ayşe "Calculate total" düğmesinin adını "Show total" yaparsa bu senaryoya ne olur? Selin bu senaryoyu okuyunca hangi iş kuralının test edildiğini anlayabilir mi? Kurumsal indirim kuralı yüzde 10'dan yüzde 12'ye çıkarsa, değişmesi gereken satırı bulmak ne kadar sürer?
BDD: Bir Araç Değil, Bir Konuşma
BDD (Behaviour-Driven Development; davranış odaklı geliştirme) adını Dan North'un 2006'da Better Software dergisinde yayımlanan "Introducing BDD" yazısından alır. North'un çıkış noktası TDD (Test-Driven Development; önce testi yazıp sonra kodu yazma pratiği) öğretirken yaşadığı bir sorundu: "test" kelimesi insanları yanlış sorulara yöneltiyordu. Test yerine davranıştan, sistemin bir sonraki adımda ne yapması gerektiğinden bahsetmek konuşmayı netleştiriyordu. Given-When-Then kalıbı da aynı yazıda, North ve Chris Matts'in story'lerin kabul koşullarını senaryo olarak yazmak için kullandığı bir şablon olarak geçer.
Cucumber'ın BDD dokümanı bu pratiği üç parçaya ayırır ve bu ayrım serinin son üç yazısının da haritası. Discovery: somut örnekler üzerinden konuşup sistemin ne yapacağında anlaşmak; 05. yazıdaki Example Mapping tam olarak buydu. Formulation: bu örnekleri hem insanın hem bilgisayarın okuyabileceği yapılandırılmış bir dokümana dönüştürmek; bu yazının konusu. Automation: her örneği sisteme bağlayıp otomatik bir teste çevirmek; 08. yazının konusu.
Sıra önemli. Gherkin bir test otomasyon dili gibi görünür, çünkü sonunda bir test runner onu çalıştırır. Ama Gherkin'in asıl okuru test runner değil, Selin ve Hakan'dır. Discovery yapılmadan yazılan bir feature dosyası, otomasyon ekibinin kendi kendine yazdığı ve kimsenin okumadığı bir test script'ine dönüşür. Emre'nin senaryosu bunun küçük bir örneği: çalışıyor, ama hiçbir iş kuralını söylemiyor.
Example Map'ten Feature Dosyasına
05. yazının example map'i Gherkin'e neredeyse birebir taşınır. Sarı kart bir Feature olur, her mavi kart bir Rule, her yeşil kart bir Scenario. Kırmızı kartlar dosyaya girmez: cevabı bilinmeyen bir şey çalıştırılabilir bir örnek olamaz. Yuvarlama sorusu muhasebeden cevap gelene kadar dosyada yoktu; cevap gelince kendi senaryosuyla eklendi. Bookstore ekibinin "Kurumsal indirim" story'si ve 05. yazıda Release 1'e kalan "stok yetmezse sipariş bütün olarak bekler" dilimi için yazdığı dosya şu:
@release-1
Feature: Institutional order pricing
Schools and libraries order a whole term's books in one institutional order.
The minimum quantity and the institutional discount keep these orders
worthwhile for both the institution and Bookstore.
Background:
Given the catalog contains these books:
| ISBN | Title | List price | Stock | Campaign discount |
| 9786050000016 | Grade 5 Mathematics | 250.00 | 500 | 0% |
| 9786050000023 | Grade 5 Science | 84.95 | 500 | 0% |
| 9786050000030 | Short Stories Reader | 120.00 | 15 | 25% |
Rule: An institutional order contains at least 20 copies in total
Scenario Outline: The minimum counts copies across all titles
When the institution orders:
| Title | Copies |
| Grade 5 Mathematics | <maths> |
| Grade 5 Science | <science> |
Then the order is <outcome>
Examples:
| maths | science | outcome |
| 19 | 1 | accepted |
| 10 | 10 | accepted |
| 12 | 7 | rejected |
Scenario: The institution is told how far below the minimum it is
When the institution orders 19 copies of "Grade 5 Mathematics"
Then the order is rejected because "at least 20 copies; this order has 19"
@REQ-BULK-05
Rule: Institutions get 10% off the list price unless a campaign gives more
Scenario: The institutional discount is applied to every line
When the institution orders 40 copies of "Grade 5 Mathematics"
Then "Grade 5 Mathematics" is charged 9000.00 with a 10% discount
And the order total is 9000.00
Scenario: A campaign discount is not added on top of the institutional discount
When the institution orders 20 copies of "Short Stories Reader"
Then "Short Stories Reader" is charged 1800.00 with a 25% discount
Scenario: Each line total is rounded to the cent, half away from zero
When the institution orders:
| Title | Copies |
| Grade 5 Mathematics | 20 |
| Grade 5 Science | 3 |
Then "Grade 5 Science" is charged 229.37 with a 10% discount
And the order total is 4729.37
Rule: Every line must match a book in the catalog
Scenario: An unknown ISBN is reported with its line number
When the institution submits this ISBN list:
| ISBN | Copies |
| 9786050000016 | 20 |
| 9786059999991 | 5 |
Then the order is rejected because "Line 2: ISBN 9786059999991 was not found in the catalog."
Scenario: A hyphenated ISBN matches the same book
When the institution submits this ISBN list:
| ISBN | Copies |
| 978-605-00-0001-6 | 20 |
Then the order is accepted
Rule: If any title is short of stock, the whole order waits and ships once
Scenario: A short title holds the whole order
When the institution orders:
| Title | Copies |
| Grade 5 Mathematics | 20 |
| Short Stories Reader | 30 |
Then the order is accepted
And the whole order waits until every title is in stock
Kitap adları ve fiyatlar kodda İngilizce, ama sayılar 05. yazıdaki yeşil kartların aynısı: 12 + 7 reddediliyor, 40 adet matematik 9.000, kampanyalı kitap yüzde 25, üç adet fen 229,37. Senaryoların hiçbiri bir düğmeden, bir sayfadan ya da bir HTTP durum kodundan bahsetmiyor; hepsi kurumun ne sipariş ettiğini ve sonucun ne olduğunu söylüyor.
Dosyanın kendisi bu hâliyle bir test. 08. yazıda yazacağımız step definition'larla Reqnroll bu dosyayı on test olarak çalıştırıyor; Scenario Outline'ın üç satırı ayrı test sayılıyor:
Test run summary: Passed!
total: 10
failed: 0
succeeded: 10
skipped: 0
duration: 852ms
Feature, Rule, Background ve Step'ler
Feature dosyanın başlığıdır; altındaki serbest metin Gherkin için yalnızca açıklamadır ve çalıştırılmaz. Bu metni story'nin "neden"ini yazmak için kullan: kurumsal siparişin iki tarafa da neden değerli olduğu. Kod okuyan biri dosyayı açtığında önce bunu görür.
Rule, Gherkin'e 6. sürümde eklenen ve bir iş kuralını, o kuralı gösteren senaryolarla birlikte gruplayan keyword'dür. Example Mapping ile en doğal eşleşmeyi sağlayan şey bu: her mavi kart bir Rule. Rule olmadan aynı dosya düz bir senaryo listesine döner ve hangi senaryonun hangi kuralı gösterdiği, ancak senaryo adlarından tahmin edilebilir.
Background, dosyadaki her senaryodan önce çalışan ortak adımlardır. Burada katalogdaki üç kitabı tanımlıyor; böylece her senaryo yalnızca kendi farkını söylüyor. Gherkin referansı Background'u kısa tutmayı, dört satırı geçiyorsa ayrıntıları daha üst seviye adımlara taşımayı önerir. Bizim Background'umuz tek bir adım ve bir tablo; tabloyu okuyan herkes senaryolardaki fiyatların nereden geldiğini görüyor.
Step'ler Given, When ve Then ile başlar; And ve But bir önceki keyword'ün devamıdır. Given bir bağlam kurar, When tek bir olayı anlatır, Then beklenen sonucu söyler. Referans her senaryo için üç-beş adım önerir; bizim senaryolarımız iki-üç adım, çünkü bağlamın çoğunu Background taşıyor. Senaryoların bazılarında Given yok: bağlam Background'da kuruluyor ve senaryonun söyleyecek ayrı bir bağlamı yok.
Dosyanın başındaki @release-1 ve kural üstündeki @REQ-BULK-05 birer tag. Tag'ler senaryoları gruplar ve seçerek çalıştırmayı sağlar. İlk yazıda requirement'lara REQ-BULK-03 gibi kısa kimlikler vermeyi önermiştik; o kimliği bir Rule'a tag olarak koymak, requirement'tan çalışan teste giden izi tek aramaya indiriyor. 08. yazıda bu tag'le yalnızca o kuralın senaryolarını çalıştıracağız.
Scenario Outline, Examples ve Data Table
Minimum adet kuralının üç örneği aynı davranışı farklı sayılarla anlatıyor. Bunu üç ayrı senaryo olarak yazmak yerine bir Scenario Outline ve bir Examples tablosu kullanılıyor: tablodaki her satır, <maths> gibi açılı parantezli yer tutucuları doldurarak bir senaryo üretir. Yer tutucuların step metninde değil, step'in data table'ında durabildiğine dikkat et; bu sayede adım bir kez yazılıyor, iki kitabın adetleri satırdan geliyor.
Outline'ın doğru kullanımı, aynı kuralın sınırını birkaç noktada göstermek. Tabloda üç satır var: tam sınırda iki kitaba bölünmüş 20 adet, eşit bölünmüş 20 adet ve bir eksik 19 adet. Sınırın iki yanı ve bir ilginç durum; 05. yazıda Example Mapping için söylediğimiz kural burada da geçerli. Reddedilen siparişte kuruma ne söylendiği ise ayrı bir senaryo, çünkü ayrı bir davranış: mesajın içeriği.
Data table ise bir adıma liste veya tablo biçiminde veri vermenin yolu. Katalog, sipariş satırları ve ISBN (kitabın uluslararası standart numarası) listesi bu dosyada tablo; Gherkin tablonun ilk satırını başlık olarak okumayı zorunlu tutmaz, bu karar step definition'ındır. Tabloyu, okuyanın bir bakışta anlayabileceği kadar dar tut. Katalog tablosundaki beş sütunun her biri en az bir senaryoda kullanılıyor; kullanılmayan bir sütun, okuyanı neyin önemli olduğu konusunda yanıltır.
Declarative ve Imperative Senaryo
Emre'nin senaryosu imperative: kullanıcının ekranda ne yaptığını adım adım anlatıyor. Aynı davranışın declarative hâli dosyadaki "The institutional discount is applied to every line" senaryosu: kurum 40 adet sipariş eder, satır yüzde 10 indirimle 9.000 olur. Birincisi nasıl yapıldığını, ikincisi ne olduğunu söylüyor.
Farkın üç somut sonucu var. Arayüz değiştiğinde declarative senaryo değişmez, çünkü arayüzden bahsetmiyor; "Calculate total" düğmesinin adı değişince Emre'nin senaryosu kırmızıya döner ama kural değişmemiştir. Kural değiştiğinde ise declarative senaryoda değişecek satır bellidir: yüzde 10 yazan satır. Üçüncüsü, declarative senaryo farklı seviyelerde otomatikleştirilebilir. Bizim senaryolarımız 08. yazıda tarayıcı olmadan, doğrudan API'ye bağlanacak; aynı metin istenirse bir gün bir uçtan uca testle de çalıştırılabilir.
Pratik bir test: senaryoyu, arayüzü hiç görmemiş Hakan'a oku. Hakan "evet, kural bu" diyebiliyorsa senaryo declarative'dir. "Hangi düğme?" diye soruyorsa senaryoda iş dili yerine arayüz dili var.
Türkçe Keyword mü, İngilizce mi?
Gherkin'in keyword'leri birçok dile çevrilmiştir ve Türkçe de bunlardan biri. Dosyanın ilk satırına # language: tr yazınca keyword'ler Türkçe okunur. Aynı minimum kuralı Türkçe keyword ve Türkçe step metniyle şöyle yazılabilir; bu dosyayı da Reqnroll ile ayrıştırıp doğruladık:
# language: tr
Özellik: Kurumsal sipariş fiyatlandırması
Kural: Kurumsal sipariş toplamda en az 20 adet içerir
Senaryo: 20 adedin altındaki sipariş reddedilir
Diyelim ki katalogda "Grade 5 Mathematics" kitabı var
Eğer ki kurum bu kitaptan 19 adet sipariş ederse
O zaman sipariş "en az 20 adet" gerekçesiyle reddedilir
Ve kuruma 1 adet eksik olduğu söylenir
İki seçenek var. Türkçe keyword ve Türkçe step metni, senaryoyu Hakan'ın ve Gül'ün doğrudan okuyabileceği hâle getirir; bedeli, step definition'lardaki eşleşme metinlerinin Türkçe olması ve kodun geri kalanının İngilizce olduğu bir projede iki dilin aynı dosyalarda karışmasıdır. İngilizce keyword ve İngilizce step metni ise kodla tutarlıdır, arama ve tooling sorunsuz çalışır; bedeli, İngilizce okumayan stakeholder'ın senaryoyu bir başkası aracılığıyla okumasıdır.
Bookstore ekibi İngilizce'yi seçiyor, çünkü projede kod İngilizce kuralı var ve senaryoları düzenli okuyan Selin ile Hakan İngilizce okuyabiliyor. Türkçe açıklama story'nin kendisinde, 05. yazıdaki kurallar ve örneklerle yaşamaya devam ediyor. Karar senin ekibinde farklı çıkabilir; önemli olan, kararı bir kez vermek ve bütün feature dosyalarında aynı dili kullanmak. Reqnroll'da varsayılan feature dili reqnroll.json dosyasındaki language.feature ayarıyla da belirlenebilir.
Gherkin Anti-Pattern'leri
Arayüze bağlı step'ler: "I click", "I type into", "I should see in the element". Bunlar Emre'nin senaryosundaki sorun; senaryoyu kırılgan ve okunmaz yapar. Arayüz testinin bir yeri var, ama o test Gherkin'de değil, uçtan uca (E2E) testlerin anlatıldığı Integration ve E2E Test serisinde göreceğimiz Playwright testlerinde daha iyi yaşar.
Uzun Background: on satırlık bir Background, her senaryonun gerçek bağlamını gizler. Okuyan biri senaryodaki sonucun hangi Background satırına bağlı olduğunu bulmak için yukarı aşağı gider. Background'da yalnızca her senaryonun gerçekten ortak olan bağlamı kalsın; bir senaryo için önemli olan bir ayrıntı o senaryonun kendi Given'ında görünsün.
Her şeyi Scenario Outline'a koymak: "Kurumsal siparişler" diye tek bir outline ve on iki sütunlu bir Examples tablosu, minimum adedi, indirimi, kampanyayı ve stoku aynı satırlarda test eder. Hangi satırın hangi kuralı gösterdiği kaybolur, bir satır kırmızıya döndüğünde neyin bozulduğunu anlamak için tabloyu çözmek gerekir. Outline bir kuralın sınırını göstermek için var; farklı kurallar farklı senaryolarda yaşar.
Teknik terimlerle dolu senaryo: "Then the response status is 422 and the JSON body contains errors.lines". Bu satır Selin için anlamsız ve API'nin bir uygulama ayrıntısına bağlı. İş dilindeki karşılığı "the order is rejected because ..." ve HTTP ayrıntısı step definition'ın içinde kalır.
Birden fazla When ve Then çifti: "When ... Then ... When ... Then ..." biçimindeki bir senaryo iki davranışı test ediyordur. İlk Then kırıldığında ikinci davranış hiç denenmez. İki senaryoya böl.
Üretimde Dikkat
Feature dosyaları kodla aynı repository'de, test projesinin içinde durur ve kodla birlikte review edilir. Bookstore ekibinde bir feature dosyasını değiştiren pull request'e Selin reviewer olarak ekleniyor; kod satırlarını değil, senaryoları okuyor. Bir kural değiştiğinde önce senaryo değişir, sonra kod; senaryo değişmeden kod değişirse CI'daki kırmızı test bunu yakalar.
Senaryolar her CI build'inde çalışır. Kırmızı bir senaryo iki şeyden birini söyler: ya kod kuraldan saptı ya da kural değişti ve dosya güncellenmedi. İkinci durumda doğru tepki testi susturmak değil, kuralın sahibine sormaktır.
Feature dosyası sayısını özelliğe göre tut, ekrana göre değil. "Kurumsal sipariş fiyatlandırması" bir feature dosyası; "Sipariş ekranı testleri" değil. Dosya adları ve Feature başlıkları, ileride living documentation olarak okunacak bir içindekiler tablosudur.
Sık Düşülen Tuzaklar
İlk tuzak Gherkin'i yalnızca test otomasyon ekibinin yazdığı bir dosya formatına indirmek. Senaryolar Example Mapping olmadan, sprint'in sonunda, kod bittikten sonra yazılırsa BDD'nin discovery ve formulation adımları atlanmış olur; elde kalan, normal testlerden daha zahmetli bir test yazma biçimidir. Bu durumda xUnit testleri daha ucuz ve daha dürüst bir seçimdir.
İkincisi her test case'i bir senaryoya çevirmek. ISBN normalleştirme fonksiyonunun on beş kenar durumu, Selin'in okuması gereken senaryolar değil; birim testlerde yaşar. Test Stratejisi serisinin "Test Pyramid, Testing Trophy ve Honeycomb" yazısındaki dağılım fikri burada da geçerli: senaryolar iş kuralını gösteren birkaç örnek için, ayrıntılı kenar durumları birim testleri için.
Üçüncüsü senaryoyu veritabanı durumuyla doğrulamak: "Then the Orders table has a row with Status = 2". Bu bir uygulama ayrıntısı ve tablo yapısı değiştiğinde senaryo kırılır. Sonucu kullanıcının ya da iş tarafının gözlemleyebileceği biçimde söyle: sipariş kabul edildi, toplam şu, sipariş stok bekliyor.
Dördüncüsü senaryoları bir kere yazıp dondurmak. Production'da bir okulun beklenmedik siparişi yeni bir örnek ortaya çıkarırsa, o örnek önce example map'e, sonra feature dosyasına girer. Dosya, sistemin bugünkü davranışının kaydı olarak yaşar.
Küçük Bir Deneme
★☆☆ Projendeki bir imperative senaryoyu ya da bir manuel test adımları listesini al ve declarative bir senaryoya çevir. Arayüz kelimelerinin (tıkla, yaz, sayfa, düğme) hiçbiri kalmasın.
★★☆ 05. yazının Küçük Bir Deneme bölümünde çıkardığın example map'i bir feature dosyasına çevir: sarı kart Feature, mavi kartlar Rule, yeşil kartlar Scenario. Aynı kuralın sınırını gösteren örnekleri bir Scenario Outline'da topla.
★★★ Bir feature dosyasını, kodu hiç görmemiş bir stakeholder'a oku. Hangi satırda "bu ne demek?" diye sorduğunu not et ve o satırları iş diliyle yeniden yaz. Sonra aynı dosyayı bir geliştiriciye ver ve senaryolardan hangi kodu yazacağını anlayıp anlamadığını sor.
Feature dosyamız hazır ve iş dilinde. Ama bu yazıda gösterdiğimiz "on test geçti" çıktısının arkasında henüz görmediğimiz bir katman var: bu cümleleri Bookstore API'sine bağlayan kod. Serinin son yazısında Reqnroll 3'ü kuracak, step definition'ları ve binding'leri yazacak, senaryoları WebApplicationFactory üzerinden API'ye bağlayacak ve living documentation raporunu üreteceğiz.