Yazılım Testleri : “Ne” ve “Nasıl”?

Süleyman Fazıl Yeşil
17 min readSep 25, 2018

--

Bu yazı, yazılım testlerinin veya birim testlerin gerekli olup olmadığını tartışmıyor. Kod taşıma akışının başına kontrol etsin diye konulmuş otomatize testlerin önemini veya hayatiyetini de tartışmıyor. Yazılım geliştirirken yazılımcıya verdiği hızlı geri bildirim, bitiş çizgisini görebilme, güven hissi veya önceden geliştirilmiş kodlara dokunma korkusunu yenebilme gibi faydalardan da bahsetmiyor.

Bu yazı, “neden” test yazmaya ihtiyaç duyduğumuza değil, testlerin “ne” olduğuna ve “nasıl” etkili testler yazabileceğimize odaklanıyor.

İlk olarak kurumsal yazılımların en büyük derdi olan karmaşıklıkla mücadeleyle ilgili olarak, edinilen tecrübeden yola çıkarak test piramidini anlatmakla başlıyor. Ardından birim testlerin anatomisini açıklamaya çalışıyor. Peşinden gelen birkaç bölümde bağımlılıkla mücadele, izolasyon, test dublörü, servis sanallaştırma konularına değiniyor. Sonra mikroservisler sebebiyle yeniden gündemimize giren müşteri odaklı kontrat testlerini (CDC) anlatıyor. Son olarak da dört dil için önde gelen test kütüphanelerini belirterek sonlanıyor.

Zaman su gibi akıyor. Finans sektöründe çalışmaya başlayalı yaklaşık üç yıl olmuş. Yeni başlamış sayılabilecek bir bankacılık dönüşüm programına dahil edildim. Bir tarafta yaklaşık 15 yıllık devasa bir bankacılık yazılımı, tecrübe, bilgi birikimi, pratikler ve araçlar ekosistemi. Diğer tarafta bu devi yavaş yavaş kırpıp yeni teknolojilerle yeniden yazarak modernize etmeye, yapısal ve kültürel bir dönüşümü gerçekleştirmeye soyunmuş bir dönüşüm programı. Temelde hedeflenen modern bir yazılıma sahip olmak. Değişikliklerin daha hızlı, güvenli ve verimli bir şekilde üretim ortamına taşınabildiği çevik bir yazılıma ve bunun gerektirdiği alışkanlık, süreç ve kültürel donanıma sahip olmak. Öykünülen ana örneklerse Google, Amazon, Netflix ve benzerleri. Bir sürü yan etkiye sahip ilaçlardan oluşan modern reçetemizse: Çevik yazılım geliştirme pratikleri + CI/CD + PaaS + DDD + Rest + bulutta çalışacak şekilde yazılmış (cloud-native) mikroservisler + konteynırlar + devops + otomasyon…

Hikaye; ayaklar daha bir yere basarak, bir çırpıda her şeyin değişmeyeceğini, yeni araçların, tekniklerin, süreçlerin kendi karmaşıklıklarını, zorluklarını, açmazlarını beraberinde getirdiğini öğrenerek ama bir yandan da yeni dünyanın getirdiği nimetleri de görüp tecrübe ederek, eskiyle yeniyi birleştirerek devam ediyor.

Bakış noktası, çıkış noktası böyle. Servis yazıyoruz. Ön yüz uygulaması yazıyoruz. Bunlara elimizden geldiğince, vaktimiz yettiğince, hayati bulduğumuz kombinasyonları gözeterek test yazıyoruz.

Tekrar eden bir örüntüdür. Bir yere gidersiniz. Benimsenmiş, alışılmış belirli kalıplar vardır. Heybenizdekini heybenizde tutar, bu kalıpları siz de takip edersiniz. Eksik bulduğunuz, değiştirmek istediğiniz noktalar olur. Elinizi korkak alıştırmazsınız. Zaman içinde heybedekileri çıkarıp, doğru bildiklerinizi uygulamaya çalışır, daha iyi iş çıkarmaya çabalarsanız.

Tecrübeyle elde edilmiş bilgiyle kitabi bilgi arasındaki ilişki, ekşi eriğin yüzün buruşmasına sebep olduğunu okumakla, o ekşi eriği yemek arasındaki ilişki gibidir. Geride bıraktıklarından hoşlanmadıysanız bir daha hayatta yemezsiniz. Birisi fuzuli bir bilgi diğeriyse sizi değiştiren, hücrelerinize işleyen bir bilgidir.

Finans karmaşık bir alanmış. Martin Klepmann, DDD (Domain Driven Design) etkinliğinde yaptığı “Event Sourcing and Stream Processing at Scalesunumuna kendisinin DDD hakkında çok fazla bir bilgisi olmadığı itirafıyla başlar. Bunu da kariyerini İnternet şirketlerinde geçirmesine bağlar. İnternet şirketlerinde DDD kavramının pek bilinmediğini ama kurumsal şirketlerde epey bilindiğini söyler. Çünkü internet şirketlerinin temel derdi ölçeklenebilirlik (1 milyon, 1 milyar kullanıcıya hizmet verebilmek, hız ve hacim), kurumsal şirketlerin temel derdiyse işin karmaşıklığı, değişen mevzuatlara uyabilmek, günlük hayatın birebir izdüşümü olan karmaşık yazılımı ve veriyi yönetebilmektir. DDD yaklaşımının babası Eric Evans’ın 2004 yılında yayımladığı, bu alandaki ilk kitabın ismi de “Domain-Driven Design: Tackling Complexity in the Heart of Software” yani yazılımın kalbindeki karmaşıklıkla mücadele etmenin bir yolu olarak iş birimi, işin uzmanları tarafından yazılım tasarımının yönlendirilmesidir. Günlük iş dilinin yazılıma yansıması, hem teknik hem iş biriminin tarafından kullanılan ortak bir dil, işin doğasına uygun olarak bölünmüş modüller ve bunlar arasındaki iletişim ve etkileşim vs.

Geliştirilen, karmaşık iş kurallarını içeren bir finansal servise test yazmak kolay değildir. Bir parametre servisini, bir ürün kataloğu servisini test etmek kolaydır. Bağımlılıklar çok fazla değildir. Karmaşık iş kuralları yoktur. Yönetilmesi gereken tonlarca istisna yoktur. Veritabanından alır, biçim verir ve iletir. Bu şekilde al ver yapan servislere, servise dışarıdan bakarak test yazabiliriz. Kombinasyonlar azdır. Testin hazırlık maliyeti nispeten düşüktür. İhtiyaç duyulan test sayısı azdır.

Karmaşık, bir sürü modüle bağımlılığı olan, bir sürü istisnaya sahip bir servisi dışarıdan test etmek kolay değildir. Çünkü en küçük kod biriminden dışarıya doğru çıktıkça, teste mevzu olan kod satırlarının sayısı, bağımlılıklar, istisnalar arttıkça kombinasyonlar da çığ gibi büyür. Zaten tek bir kombinasyona test yazmak epey maliyetliyken iş kopyala -> yapıştır -> senaryo için gerekli değişikliği yap -> testi çalıştır -> senaryo için sonuçları kontrol et akışına dönüşür. Sonra “kopyala-yapıştır” yapmaktan mutsuz olup bu kısımları ortak test hazırlık fonksiyonlarına (object mother) dönüştürürüz. Ama bu da mutsuz eder, her bir kombinasyona uygun parametrik bir fonksiyon yazmak da güçtür. Yeni bir istek gelir. Bu kez o eski ortak fonksiyonu değiştirmek yerine onu kopyalayıp değiştirip kullanırız.

Bu da mutsuz eder. Hem testler karmaşık anlaşılması güç hale gelir. Hem de servise dışarıdan yazılan testlerin koşması çok uzun sürmeye başlar. Önce 2dk sürer. Sonra 3dk. Sonra 5dk. Her bir yeni değişiklik, istisna için dışarıdan test yazmaya kalksak testlerin çalışma süresi alıp başını gidecek. Halbuki ihtiyaç duyduğumuz şey küçük bir değişiklik yapmak ve bir şeyi bozup bozmadığımızın bilgisini hemen alabilmek. Her ne kadar test kütüphanesine (test runner) şu testlere odaklan, yalnızca onları çalıştır diyebilsek de, tüm testleri çalıştırmadan kodları gönderemiyoruz (en azından prensip olarak :)). Yavaşlık hem bizi yavaşlatıyor, hem de bizi bekleyenleri.

Farklı bir çok bağlamda ifade edilegelen bir deyiştir: Böl, parçala, yut. Elmayı bir bütün olarak ağzımıza atıp çiğnemeye çalışmak yerine, dilimleyip küçük lokmalar halinde yemeye çalışırız. Yani birim olarak servisi (API) almak yerine, içeriye gireriz ve anlamlı dilimlere (class/struct/fonksiyon) bölmeye çalışırız. Motoru, tekerlekleri, lastikleri, freni üretip arabaya taktıktan sonra 100km hıza çıkıp freni test etmek yerine freni ayrıca, lastikleri ayrıca, motoru ayrıca üretip ayrıca test edip sonra birleştiririz, bütüne bir entregrasyon testi yapıp kalibre ederiz. Fabrika testinden geçip gelen motoru araca taktığımızda biliriz ki kontağı çevirdiğimizde motor alev almaz, patlamaz, çatlamaz, sızdırmaz.

İfade etmeye çalıştığım, karmaşıklığa göre içeri-dışarı ekseninde yakınlaşıp uzaklaşarak (zoom in/out), karmaşıklığı ve boyutu yönetmenin gerekliliğidir. Test ettiğimiz kod büyüyüp yönetilemez hale gelmeye başlayınca şefin keskin bıçağını elimize alır ve biçip doğrarız. Hücreler gibi doğal büyüklüğe ulaşınca böler, yeniden modelleme yapar, yeniden tasarlarız. Derler ki “esasında tüm modeller yanlıştır, fakat bazıları kullanışlıdır” (George Box). Hiçbir model gerçeği tam yansıtmaz, bir modelin geçerliliği eldeki problemi çözmeye yardım ettiği kadardır.

Test Piramidi

Tekerleği yeniden keşfetmiyoruz. Yazılım alanındaki problemler farklı zamanlarda, farklı bağlamlarda yeniden su yüzüne çıkar. Tarihsel akışı bilmediğimizde bu bize yepyeni bir problemmiş gibi gelir.

Testlerin kapsam ve büyüklük bakımından sınıflandırılmasıyla ilgili farklı test katmanlarını ifade eden “Test Piramidi” metaforu kullanılmaktadır.

Resim: Test Piramidi, Kaynak: Sevgili Google

Kavramı ortaya atan Mike Cohn. Sınıflandırmayı birim, servis ve arayüz şeklinde yapmış. Birim testler servisi oluşturan kod birimleri, parçaları için yazılmış testler. Servis testleri ise API seviyesindeki testler. Arayüz testleri ise kullanıcı arayüzüne yazılmış olan testler.

Terminoloji üzerinde günümüzde bir görüş birliği bulunmuyor. Neyin birim sayılacağı, neyin entegrasyon testine gireceği biraz bulanık. Kesin olan bir şey varsa o da, olası en küçük birim olan bir fonksiyondan en dışarıya kullanıcı arayüzüne doğru gittikçe kapsam, büyüklük ve karmaşıklık artıyor. Dışarıya doğru gittikçe hız azalıyor, karmaşıklık ve bunun sonucunda test maliyeti artıyor.

Resim: Ters test konisi, bir hatalı kullanım örneği (anti-pattern). Kaynak: Sevgili Google.

Kullandığımız kavramlar önemli olmakla birlikte, asıl önemli olan günün sonunda pragmatik davranarak, getirisini götürüsünü düşünerek, hangi seviyede ne kadar test yazacağımıza, test stratejimize karar vermektir. Tüm olası kombinasyonları arayüz üzerinden test etmek için günlerce test kodu yazmaya çalışıp, saatlerce tüm senaryoların sonlanmasını bekleyebiliriz. Veya tüm testleri en iç katmanda en küçük parçalara yazıp, sistemin bir bütün olarak çalıştığını düşünüp sonra en basitinden ürün servisinin ürün numarasını “productId” ismiyle göndermesi, önyüzünse “id” ismiyle beklemesi sonucu ekranın çalışmadığını görebiliriz. İki doğru bazen bir doğru etmeyebilir.

Doğru ne bir uçta, ne de diğer uçta. Servisin, sistemin karmaşıklığına bağlı olarak ortalarda bir yerlerde.

Bir Testin Anatomisi

Kaynak: Sevgili Google

Bir test 4 aşamadan oluşur:

  • Hazırlık aşaması
  • Testin çalıştırılması
  • Sonuçların kontrolü ve doğrulaması
  • Testte kullanılan yapıların, verilerin temizlenmesi

Hazırlık aşamasında test edilecek sistem senaryoya uygun bir şekilde hazırlanır. Ardından test çalıştır, test edilecek sistem, servis, modül, nesne, fonksiyon çağrılır. Peşinden gelen sonuç, beklentiler doğrultusunda kontrol edilir. Son olarak nesne, servis, veri temizliği yapılır. Eğer sonuçlar beklediğimiz gibiyse test geçer, değilse test geçmez, kırılır.

Bir testte en uğraştırıcı bölüm genellikle hazırlık aşamasıdır. Test edilecek sistem, servis, nesne veya fonksiyon, test senaryosu oluşturulup çağrılmaya hazır hale getirilir. Bağımlı olunan şeyler; veri tabanı, kullanılan diğer servisler, modüller, nesneler hazırlanır veya ileride değineceğimiz üzere dublör kullanılarak izole edilir. Test kapsamına, bağımlılıklara göre bu aşama çok uzun sürebilir ve uğraştırıcı olabilir. Az da olsa bazen de sonuçları kontrol etmek, doğrulamak için takla atmak durumunda kalabiliriz.

5 alandan oluşan bir tabloya kayıt atmak kolay olabilir. Ama tonlarca koşulu, kriteri, iş kuralı olan, 20–30 kolondan oluşan tablolara test senaryosu için uygun kayıtları atmak zaman alır.

5 tane alandan oluşan bir JSON nesne bekleyen bir servise girdi hazırlamak kolaydır. Ancak 20–30 tane alan içeren, uygun ve geçerli bir JSON nesne hazırlamak zaman alır. Kurumsal uygulamalar, veri yoğun ve bu anlamda karmaşık uygulamalardır.

Testlerin her bir aşamasında yapılan işleri kolaylaştırmak üzere yazılmış açık kaynak yardımcı kütüphaneler bulunmaktadır.

Testlerin temel yapı taşları olan :

  • Testlerin yazılması
  • Testlerin koşulması (test runner)
  • Test verisi hazırlama
  • Test edilecek birimi izole etmek ve bağımlıkları yönetmek (test doubles)
  • Beklentileri ifade edilerek doğrulamaların yapılması (matchers)
  • Test koşum sonuçlarının raporlanması
  • Test kapsama raporu oluşturulması (coverage)

gibi şeyleri kendimiz de yazabiliriz.

Ancak yetenekleri farklı olmakla birlikte hemen tüm yazılım dillerinde bu ihtiyaçları karşılayan test kütüphaneleri bulunmaktadır. Bazı test kütüphaneleri bunların çoğunu sağlar. Ama genellikle birden fazla test kütüphanesini bir arada kullanırız ki sofistike şeyleri kolaylıkla yapabilelim. Örneğin bir karmaşık bir JSON nesnesinin içinde bir belirli bir alan var mı, varsa değeri ne diye bakmak için bu iş özelleşmiş bir JSONPath kütüphanesi kullanırız. Veya JSON formatlı verinin verilen bir JSON şemaya uygunluğunu kontrol etmek için başka bir kütüphane kullanırız veya bulamadıysak bunun için var olan kütüphaneleri kullanıp kendimiz bir yardımcı kütüphane yazıp paylaşabiliriz.

Bir dilde test yazmayı biliyorsak, belirli bir sistematiği oturtmuşsak bunu diğer dillere de uygulayabiliriz. Bu noktadan sonrası o dilin söz dizimi ve gramerini (syntax) öğrenmekten ibarettir büyük ölçüde. Benzer şey normal yazılım kodu için de geçerlidir. Biri Intercepting Filter der, biri Servlet Filter der, biri Interceptor der, bir diğeri Middleware der. Hepsinin yaptığı şey aynıdır, belirli bir kod çağrım zincirinde araya girip isteği karşılamak ve tüm istekler için ortak bir fonksiyonu icra etmektir (cross-cutting concerns). Kalıp aynı, yaklaşım aynı, sadece isimler farklı.

Basit bir test Go ile. Gerçekte daha kısa yazılabilir bu test, tablo ile ifade edilerek (Table Driven Test), ayrıca temizlik kısmına da gerek yok bu test için. Daha kompakt olarak da ifade edilebilir. BDD tarzıyla yazıldığında, XUnit tarzına göre daha açıklayıcı fakat biraz uzun oluyor testler. Seviyoruz BDD stilini.

BDD stiliyle test
XUnit stiliyle test. Ayrıca tablo şeklinde ifade edilmiş testlere bir örnek (table driven).

İzolasyon yahut Bağımlılıklarla Mücadele :)

Bir kodu test etmek istediğimizde, kodun bağımlılıklarıyla başımız derde girebilir. İster bir servise dışarıdan test yazıyor olalım, isterse de servisin içindeki bir fonksiyona veya nesneye.

Temel olarak;

  • karmaşık bağımlılıklar ve test edilecek nesneyi hazırlamanın uğraştırması
  • bağımlı olunan birimin kontrolümüz dışında olması (başka bir servis, SDK vs.)
  • asenkron çalışan bağımlı olunan birimle
  • db ve servis çağrılarının yavaş olması
  • karmaşık senaryoların veya hata senaryolarının kolayca yaratılamıyor olması (kontrol dışı olması veya elle müdahale isteyip otomatize edilememesi veya çok uğraştırması)

gibi sebeplerden dolayı test ettiğimiz birimi bağımlı olduğu nesnelerden, servislerden, veri depolarından soyutlamak, izole etmek isteriz.

Örneğin gidilen bir servisin 504 zaman aşımı hatası verdiği durumda, bizim servisimizin beklediğimiz gibi davranıp davranmadığını test etmek istiyoruz. Bu durumu nasıl test ederiz?

Veya veri tabanından 30'ar kolon içeren 10 farklı tablodan kayıt okuyan, okuduğu kayıtların durumuna göre farklı farklı işler yapan karmaşık bir kodu nasıl test ederiz?

Veya SMS göndermek için dış SMS servisine giden bir kodu nasıl test ederiz? Gönderilen SMS içeriği doğru mu? SMS servisi hata gönderirse doğru davranıyor mu?

Veya Kafka’daki bir topic’i dinleyen ve arka planda çalışan bir kodu nasıl test ederiz? Kafka kurup ilgili topic’e mesaj atıp veri akıtarak mı? Her senaryo için mi? Peki hata durumları? Kafka’ya nasıl hata ver diyeceğiz? Belirli bir hatayı vermesini nasıl sağlayacağız?

Veya on satır kodu test etmek için yüz satır test kodu yazmak zorunda kalıyorsak bir sürü nesneyi hazırlayıp test ettiğimiz birime vermek için.

Bir seferde bir davranışı test edebilmek için, test ettiğimiz birimin doğru davrandığından gerçekten emin olmak için, kolay test edebilmek için, bu işleri otomatize edebilmek için, test ettiğimiz birimi bağımlılıklarından izole edip girdi ve çıktılarını kontrol etmeye ihtiyaç duyarız.

Resim: Bağımlılıkların enjekte edilmesi (DI). Kaynak: Sevgili Google

Bunu yapabilmek için de test edilebilir kod yazmak, arayüzlere (interface) karşı programlama yapmak, bağımlılıkları dışarıdan enjekte etmemiz (dependency injection) gerekir. Böylelikle test ettiğimiz birim için, arayüze uyduktan sonra, hangi nesneyi verdiğimizin hiç bir önemi yoktur. DB ile iletişim kurup veri okuyup yazan Repository nesnesinin gerçekten DB’den mi okuduğunun veya bellekteki bir değeri mi verdiğinin hiç bir önemi yoktur. Veya Http isteği yapıp bir servisten bir bilgi alan bir proxy nesnesinin gerçekten ilgili servise mi gittiğinin, yoksa bir sanal servise mi gittiğinin veya yine bellekteki statik bir değeri mi dönüp durduğunun da hiç bir önemi yoktur. Çünkü test ettiğimiz şey entegrasyonun kendisi değil içerideki uygulama kodunun iş kodunun ne yaptığıdır.

Ama küçük kapsamlı izole birim testlerin varlığı, sistemi uçtan uca olacak şekilde, farklı alt modüllerle ve nihayetinde bütünüyle test etme gereksinimini yok etmez. Bu ihtiyaca servis kendi içinde test eden API testleri, veya test ortamında tüm sistemi uçtan uca test eden kullanıcı kabul testleri cevap verir.

Mikroservis testleri bağlamında benim alışkanlığım şöyle: Eğer bir servis çok karmaşık değilse onu sadece diğer servislerden izole ederek (sanal servis: Mountebank veya diğerleri) test etmeyi yeğliyorum. Eğer karmaşık iş kuralları varsa, kombinasyonlar çoksa ve servise dışarıdan bakıp tüm senaryoları test etmek yavaş, maliyetli ve “kopyala-yapıştır”a yol açmaya başladıysa ve testlerin süresi uzamaya başladıysa içeriye girip daha küçük kapsama ve kombinasyona sahip alt birimlere test yazmayı tercih ediyorum.

Ön yüz tarafında ise Üstün Özgür üstadın yaklaşımını benimseyip iş kodu ile ekran kodunu ayırt etmeye çalışmak, test edilebilir, kolay yönetilebilir ve anlaşılır bir kod tabanı sağlıyor. İş kodu (model) gösterimden (view) ayrılınca test etmek kolay oluyor. Yine entegrasyon testlerine ihtiyaç oluyor, ancak iki tarafı ayırmak, kodun büyük bir kısmını kolay test edilebilir, anlaşılır ve yönetilir hale getiriyor. İzolasyon bakımından ise, JS dinamik tipli bir dil olduğundan bağımlı olunan herhangi bir nesne, dublörüyle kolaylıkla değiştirilebiliyor, yeter ki nesneyi dışarıdan alsın (ör: React bileşenindeki prop, veya fonksiyona gönderilen parametre). Import edilip içeride kullanılan global bağımlılıkları ise gerektiğinde, dışarıdan parametre olarak alacak şekilde ayarlamak da testlerde izolasyonu sağlama bakımından yardımcı oluyor.

Temel prensip aynı, bağımlılıkları dışarıdan verilecek şekilde ayarlayalım ki testlerde gerçek nesne yerine, kolayca elimizde oynatabildiğimiz dublörünü kullanıp izolasyonu sağlayabilelim.

Kontrol ve Simülasyon — Test Dublörü

Resim: Dublör. Kaynak: Sevgili Google

Hayatımda okuduğum diyalog şeklinde kurgulanmış, en sade, anlaşılır, güzel ve hoş bir tat bırakan teknik blog yazısı C. C. Martin imzalı “The Little Mocker”dan:

-Fakat bu mock değil mi? Ben bu test nesnelere “mock” denildiğini düşünüyordum.

- Evet öyle; ama bu argo.

- Argo?

- Evet, gündelik deyişte “mock” kelimesi gayri resmi olarak bazen testlerde kullanılan tüm nesne ailesini ifade etmek için kullanılmaktadır.

- Peki bu tip nesnelerin resmi bir ismi var mı?

- Evet, test dublörü (test doubles) deniliyor.

- Yani filmlerdeki “dublörler” gibi mi demek istiyorsun?

- Aynen.

- O zaman “mock” kelimesi günlük konuşma diline özgü bir deyiş.

- Hayır, resmi bir anlama da sahip. Fakat gündelik konuşmada “mock” ile “test dublörü” eş anlamlı kelimeler.

- Neden iki kelime var? Neden sadece “test dublörü” demiyoruz “mock” yerine.

- Tarih.

- Tarih?

- Evet, uzun zaman önce çok zeki birkaç kişi bir makale yazdı ve “Mock Object” terimini tanıtıp tanımladı. Makaleyi okumayıp bu terimi duyanlar, terimi daha geniş bir anlamda kullanmaya başladılar. Hatta kelimeyi yükleme dönüştürdüler. “Haydi bu nesneyi ‘mock’layalım” demeye başladılar.”

C. C. Martin.

Resim: Testte simülasyon yerine gerçek nesneleri kullanmak. Kaynak: Sevgili Google.

Testlerde bazen test edilen birimi izole etmeye ihtiyaç duyarız. Girdileri kontrol etmek, çıktılarını kontrol etmek, gözlemlemek, doğrulamak isteriz. Bazen dış bir sistemi daha basit bir şekilde simüle etmek isteriz.

Eğer test ettiğimiz birime olan girdileri kontrol etmek istiyorsak, yani “servis şu cevabı versin”, “callback fonksiyonu hata fırlatmış olsun”, “veritabanından 3 tane kayıt gelmiş olsun” istiyorsak, ihtiyaç duyduğumuz nesneye “stub” deniyor.

Eğer test ettiğimiz birimin çıktılarını (dışarıya yaptığı çağrılar) kontrol etmek, kayıt altına almak istiyorsak, ihtiyaç duyduğumuz nesneye “spy” deniyor. Gözlemci yani. Gözlüyor, kaydediyor, istersen kaydettiklerini sana veriyor. Gerçek bağımlıkla araya girmiş bir aracı (proxy) olarak da kurgulanabiliyor, istenen bir cevabı dönecek şekilde ayarlanmış bir “stub” nesneye de yönlendirilebiliyor.

Spy” tipindeki bir nesneyi otomatik doğrulama yapacak şekilde kurgularsak “mock” oluyor.

Gerçeği gibi davranan, ancak basit bir şekilde kodlanmış simülasyon amaçlı nesnelere “fake” deniyor. Örneğin testlerde kullanılan ve diske yazmak yerine veriyi bellekte tutan (in-memory db) h2 veritabanı.

Eğer kullanılmayacağını bildiğimiz, ne olduğunun bir önemi olmayan, sadece fonksiyon imzası gereği parametre olarak göndermek zorunda olduğumuz nesnelere de “dummy” deniyor.

Test kütüphaneleri genellikle bu ihtiyaçların hepsini karşılıyorlar. Birbirine benzer bir DSL dil sunuyorlar. Genellikle de bunlara “mock kütüphanesi” diyorlar.

İsimlendirmeler, neye ne dendiği önemli. Farkları anlamak da önemli. Ama gün içinde ben “mock” yaratayım, “stub” yaratayım demiyoruz. Sadece test ettiğimiz birimin girdi çıktısını kontrol etmeye çalışıyoruz. Soluk alıp verdiğimizin bilincinde olmamamız gibi. Susarsak su içiyoruz, acıkırsak yemek yiyoruz. Düşünmeden, otomatik olarak.

Go Mock Örneği, hikayesi şöyle:

Bir röportaj (Interview) yapılıyor. Bir soruları hazırlayan (Questioner), bir soran (InterviewMaker), bir de cevaplayan (Answerer) var. Derdimiz röportaj yapıcıyı test etmek. Bu amaçla testte soruları soranın gerçeğini, cevaplayanın ise dublörünü oluşturup röportajı yapana veriyoruz. Kurguladığımız röportajın ortaya çıkmasını bekliyoruz.

Dublör Kullanımı: İyi, Kötü ve Çirkin

Her şeyin aşırısı zararlı demişler. Testlerde test dublörü kullanımı için de geçerli bu.

İlk olarak, test edilen bir kodu, bağımlı olduğu başka bir kod parçasından izole ettiğimizde, sistemin tümünü test etmiş olmuyoruz. Ya istisnasız her bir parçayı ayrı ayrı izole edip test etmemiz gerekiyor veya parçaları entegre edip en azından bir tane başarı bir tane de hata durumu için entegrasyon testi yapmamız gerekiyor ki entegrasyon sonrası problem olmayacağından emin olalım.

İkinci olarak test edilen kodu, bağımlılıklarından izole etmek demek; test kodunun o kod hakkında çok şey bilmesi demek. Uzaklaştıkça, kuş bakışı baktıkça testler kapalı kutu testine doğru gider. Sistemi bir bütün olarak görürüz. Kutunun içinde ne değiştiğinin bir önemi olmaz. Önemli olan düğmeye basıldığında ekrana görüntünün gelmesidir. Ama bu ama aynı zamanda kutunun içindeki mekanizmanın kontrolümüzden çıkması da demektir. Yakınlaştıkça şeffaf kutu testine doğru gideriz. Caddeler, sokaklar, evler, kapı ve pencereler görünür hale gelir. Belediye, güzergahtaki evleri kamulaştırıp, tüm sokakları dikey kesen bir ana cadde yapmaya kalktığında çok şey etkilenir. Testlerin hepsi kırılmaya başlar. Söylemek istediğim bir fonksiyon imzasının değişmesi bile bir çok yeri etkiler hale gelebilir.

Kod değiştikçe testlerin de değişmesi kaçınılmaz. Bunda bir problem yok. Testler de birinci sınıf vatandaş. Normal kodlar gibi uygulamanın bir parçası ve bakımının yapılması gerekir. Problem alakasız testlerin patlaması. Bunlara kırılgan test deniyor. Bu tip testler, testlere olan güvenimizin sarsılmasına, bıkkınlığa sebep oluyor. Yakınlaştıkça ve dublör kullanarak izole ettikçe kırılganlık riski artıyor.

Çözüm; iyi modelleme yapmak ve çok fazla şey yapan nesneler oluşturmamaya çalışmakta. Kötü taraflarının farkında olarak ihtiyaç duyduğumuzda test dublörü kullanmakta. Kodun ne kadar içine gireceğimizi, ne kadar dışarıda kalacağımızı tartmaya çalışmakta.

Servis Sanallaştırma veya Sanal Servisler

Eskiden de günümüzde de büyük kurumsal sistemler bir çok parçadan oluşuyor.

Resim: Ana uygulaması monolitik (tek parça) olan bir sistem mimarisi

Modern araçlar, konteynırlar, platformlar eskiden yapmanın zor olduğu şeyleri yapmayı kolaylaştırdılar. Önce Amazon altyapısını dışa servis olarak açtı. IaaS çıktı ortaya, bulut kavramı popüler olmaya başladı. Sonra biraz daha yukarı çıkıldı ve sanal makineyle uğraşmak yerine uygulamalar düzeyinde düşünülmeye başladı. PaaS ortaya çıktı. Kodu gönder, platform çeksin derlesin ve çalıştırsın, gelen trafiğe göre elastik bir şekilde otomatik olarak uygulamanın çalışan örneklerini artırıp azaltsın. Buradan konteynırlarda çalıştırılabilen küçük servislere (mikroservis) geçildi. Önceden tek bir sanal makinede bir arada çalışan modüller (.class veya .dll) varken, şimdi modüller bağımsız ayrı servislere dönüştü, kendi bağımsız yaşam döngülerine sahip oldular. Büyük bir kütleyle uğraşmaktan, herkesin aynı kaynakları kullanmasından, aynı veritabanına gitmesinden kaynaklanan geliştirme ve bakım zorlukları, operasyonel maliyetler, donanım limitleri problem olmaktan çıktı. Bir sürü problem çözüldü. Ama yeni dağıtık yapı; baş ağrıtan, çözülmesi gereken, kendine özgü yeni bir sürü problemi de peşinden sürükleyip getirdi.

Resim: Monolitik uygulamadaki modüllerin ayrı servislere dönüştürüldüğü mikroservis mimarisi

Değişim testlere de yansıdı. Önceden nispeten daha az dış servis bağımlılıkları arttı. Modül bağımlılıkları, servis bağımlılıklarına dönüştü. Fonksiyon çağrıları, Http isteklerine dönüştü. Bir mikroservisi, diğerlerinden izole edip test edebilmek önem kazandı.

Bu amaçla kullanılan açık kaynak araçlar bulunmakta. Temelde servis test edilirken bağımlı olduğu gerçek servise gitmek yerine bir sanal servise yönlendiriliyor. Bunun için sanal servisin gerçek servis gibi davranması gerekiyor. Bunu da bize sunulan SDK’ları kullanarak, sanal servisi ayarlayarak gerçeklştiriyoruz. Sanal servise diyoruz ki, “Eğer Http Header içinde bu bilgi varsa, Url içinde şu bilgi varsa, Http Body içinde bu bilgi varsa, geriye şu JSON formatlı cevabı gönder”.

Test ettiğimiz servis gerçekte kiminle konuştuğundan habersiz. Onun önemsediği haberleştiği servisin anlaştığı gibi, beklediği gibi cevap vermesi.

Bu amaçla kullanılan güzel bir araç Mountebank. Kendini, “over wire test doubles” yani uzak servislerin dublörü olarak tanıtıyor. Gerçek sistemi taklit eden bir dublör. Hem testlerde test edilen servisi izole etmek için kullanılabiliyor, hem de bağımlı olunan o servislerden bağımsız olarak geliştirme yapmak için. Mesela ön yüz uygulamasını geliştirirken servis tarafının bitmesini beklememize gerek yok. Ne alınıp ne verileceği belli ise, yani servis kontratı belli ise Mountebank sanal servisi üzerinde bu servisi kaydedip, ilgili servis çağrılarına istediğimiz cevabın dönülmesini sağlayabiliriz.

  • “100 nolu müşteriyi isterlerse şu müşteri bilgilerini dön onlara”
  • “101'i isterlerse 404 hatası dön”
  • “102'yi isterlerse de 504 hatası dön”

Böylelikle servisin geliştirilme sürecinden bağımsız, onu beklemeden ön yüz tarafını geliştirme ve farklı kollardan eş zamanlı ilerleme yeteneği kazanıyoruz.

Mountebank dışında WireMock ve başka açık kaynak veya ticari ürünler de mevcut.

Tüketici Odaklı Kontrat Testi (Consumer Driven Contract — CDC)

Kavram aslında eski. Bir servisi tüketenin (consumer) servisi sağlayana (provider), “ben bu servisten şu biçimde cevap vermeni bekliyorum” diyerek verdiği kontrata Tüketici Odaklı Kontrat Testi deniyor. Kontratı tüketici belirliyor. “Şu şekilde istiyorum kardeşim” diyor. Servis sağlayıcı da bu kontrata uyuyor.

Modüller mikroservislere dönüşünce, modül bağımlıkları da servis bağımlılıklarına dönüştü. Dolayısıyla servisler sistemin kalbi, yukarıdan bakıldığında görülen birimler haline geldi. Servisler özne haline geldi. Temel yapı taşları haline geldi. Bunlar arasındaki ilişkiler ve bağımlılıkların yönetimi de hayatiyet kazandı. Bir servisin kontratı habersizce değiştiğinde, bu servisi kullanan diğer servislerin çalışmayacağından, servis değişimlerini kontrol etmek önem kazandı.

Yukarıdaki şekilden hareketle örneğin; müşteri bilgilerinin içinde isim bilgisi gelmezse Servis-1 hata vermeye başlar. Yaş bilgisi gelmemeye başlarsa da Servis-2. Veya bir alanın tipi, uzunluğu değişmiş olabilir, bu da tüketici servisin çalışmasını bozacaktır.

Bu noktada imdada kontrat testleri yetişiyor. Kod taşıma akışının başına bir kontrol görevlisi olarak. Temel fikir herkes beklentilerini ifade eden testleri yazıp göndersin. Bu testler bir depoda toplansın. Servis sağlayıcı bir değişiklik yaptığında önce bu kontrat testleri otomatik olarak taşıma akışının başında devreye girsin ve çalışsın. Eğer bir kontrat ihlal edilmişse taşıma dursun, kimse etkilenmesin, kesinti olmasın.

Tabi bu testleri de düzgün yazmak gerekiyor. Yukarıdaki örnekte olduğu gibi, naif bir şekilde ismin “a” gelmesini, veya birebir {“isim”: “a”, “soyisim”: “b”} şeklinde bir cevap gelmesini bekleyen bir test yazarsak; bu esnek olmayan, kırılgan bir teste sebep olur. Yeni bir alan eklendiğinde, veya isim “a” yerine “ahmet” olarak gönderildiğinde ,test sebepsiz yere kırılabilir. Dolayısıyla değer kontrolünden çok şema kontrolü yapmak, ve fazladan gelen değerleri gözardı etmek iyi olabilir.

Kütüphaneler

Bir çok dilde, testlerde kullanılmak üzere geliştirilmiş kütüphane ekosistemi bulunmaktadır.

Go:

  • Dilin içinde XUnit stili test yazmayı sağlayan bir modül ve testleri koşmayı ve raporlamayı sağlayan komut satırı araçları geliyor.
  • Ginkgo: BDD stili test yazmayı sağlıyor.
  • Gomega: Beklentileri ifade etmeyi sağlıyor.
  • go-mock: Test dublör kütüphanesi.
  • gobank: mountebank sunucusunu ayarlamayı sağlayan kütüphane (sdk).
  • Testify: Birçok fonksiyonu bünyesinde barındıran bir kütüphane: beklentileri ifade etmek, test dublörü yaratmak vs.
  • gomock, mogo, moq, mock4go: Test dublör kütüphaneleri.

JavaScript:

  • Jasmine, Mocha: BDD stiliyle test yazmayı sağlıyor. Dublör nesne yaratmak ve beklentileri ifade etmek için kendisi fonksiyon sağlıyor.
  • Karma, Jest: Test koşum, raporlama, kapsam raporu çıkarma vb. fonksiyonları sağlayan araçlar.
  • Chai: Beklentileri ifade etmeye yarıyor.
  • Sinon: Test dublör kütüphanesi.
  • Enzym: React bileşenleri için test kütüphanesi

Java:

  • JUnit: Test kütüphanesi, hem test yazmayı hem de koşmayı sağlıyor. Kent Beck ve Eric Gamma birlikte geliştirmişler.
  • Mockito, EasyMock: Test dublör kütüphanesi.
  • MockMvc: Spring Controller metotlarını (rest api veya spring mvc) test etmek için.
  • REST-assured: REST API testi ve doğrulamaları için DSL sunan bir kütüphane.
  • Hamcrest: Sofistike beklentileri ifade etmek için bir kütüphane.
  • json-path: JSON içeriğindeki beklentileri ifade etmek için bir kütüphane.

.Net Core:

  • xUnit, NUnit, MSTest: Test kütüphaneleri.
  • moq: Test dublör kütüphanesi.
  • FluentAssertions: Beklenti ifade kütüphanesi.

Bitirirken

Kapsamı geniş tutmak, kod detayına çok fazla girememeyi ve genel hatlarıyla verip geçmeyi getirdi. Kapsamı geniş tutmak doğal olarak derinlikten ödün vermeyi getirdi.

Belki konu bazlı, kısa, daha nokta atışı şeyler yazmak; dile özgü, araca özgü, kütüphaneye özgü şeyler yazmak, daha az yorucu ve detaylı olabilirdi.

Faydalı olması umuduyla.

Kaynakça

--

--