Bakar mısınız? Önden bir test söylemiştik ama…

Şef muhtemelen ana yemeğe konsantre olduğu için, sizin aperitif yemek unutuldu sanırım!

Fotoğraf, Fabrizio Magoni tarafından çekilmiş, Unsplash’den indirilmiştir.

Uygulama geliştirirken de sıklıkla — ve refleks olarak — davrandığımız üzere; isteri karşılayacak olan algoritmayı ve algoritmayı sarmalayacak olan fonksiyonu tasarlarken davranışları gözardı ediyoruz. Oysa ki geçtiğimiz zamanlarda okuduğum (referansı hatırladığım an link ekleyeceğim) ve hala aklımda olan şu cümle çok önemli;

Fonksiyonları tasarlarken birincil endişe, algoritmalar değil davranışlar olmalıdır. Davranışlar ise algoritmaların seçimi konusunda sizi doğru kısıt ve yönlendirmelere itecektir.

Bu davranış sebebiyle de test yönelimli geliştirme (Test-Driven Development) kültürüne adaptasyonumuz zorlu oluyor, hatta bazen — ne yazık ki — olamıyor.

Test-Driven Development

Önce testi yaz, sonra fonksiyonu! yaklaşımı, işin en başından beri bana hep zor ve imkansız geldi. Düşünsenize, ortada bir fonksiyon yok ve siz olmayan bir fonksiyonu, olmayan parametrelerle çağırıyorsunuz; olmayan çıktıyı, olmayan bir sonuçla test ediyorsunuz. Bu imkansızlık senaryosu, yukarıda alıntıladığım cümleciği okuyana kadar geçerliydi ve oldukça zorlandım, açıkça itiraf etmek gerek. Sonra şunu farkettim:

Bir fonksiyon yazmaya karar verdiğimde içgüdüsel olarak kodu yazmaya başlıyorum. Burada “X” değişkenini 2'yle çarpsam ve gelen sonucu DB’ye sorsam, eğer DB’den gelen sonuç istediğim gibi ise Cookie’ye “true” değerini atarım. Cookie’ye de başarıyla yazdıktan sonra nihai sonucu geri döndürürüm.

Hadi şimdi gel de bunun testini yaz!?!

Yine içgüdüsel olarak fonksiyonun patlamaması için case’ler hazırladınız, değil mi? 48. satırda null kontrolü yapmışım, dur bir null geçeyim parametreyi, bakalım patlayacak mı?

Bu durumda biz algoritmayı test ediyoruz sanki, ancak amacımız bu muydu? Birim testimizin amacı, davranışın doğru sonuç vermesi değil miydi? Nasıl bir algoritma ile ilerlediği testin neyine?

Kafamızda canlanması için hadi gelin gerçek bir örnek yapalım. Örnek fonksiyonumuz oldukça basit. QueryString’den gelen parametreyi istediğimiz tipe çevirip döndürecek. ?param=5 gelen değeri, fonksiyon integer tipinde iletebilecek.

Herhalde çoğumuz, en basit haliyle yukarıdakine benzer bir kod yazardık diye düşünüyorum. Sonra da bu fonksiyona test yazmaya karar verdiniz (film yavaş yavaş kopmaya başlıyor).

Test’i çalıştırdınız ve BAM! Hiçbir test senaryonuz başarılı değil. Test’i debug ettiniz ve gördünüz ki 10. satırda kullandığınız HttpContext.Current objesi null geliyor. Sebebi ise oldukça basit; testin koştuğu ortamda herhangi bir HttpContext yok ki!

O kadar güzel kodladığınız blok ne yazık ki test edilebilir değil ve kişisel fikrim bu sebeple “güzel” de değil.

Test yönelimli geliştirseydim, nasıl olurdu?

Şu alıntıya tekrar dönelim mi? Diyor ki “davranışlar”. Eğer önce kodu yazmak yerine davranışı düşünseydim içimden şöyle bir cümle kurardım:

Ben bir fonksiyon yazacağım. QueryString’de istediğim anahtarda bir değer varsa o değeri alıp istediğim tipte geri döndürecek. Hmmmm… O zaman benim bu davranış için iki bağımlılığım var. Request nesnesi ve string tipinde bir anahtar değeri.

İşte bu! Fonksiyon kendini ortaya çıkartmadı mı sizce de? Bu metod iki parametre almalı. HttpContext ve string tipinde iki parametre. Hadi gelin şu kodu refactor edelim:

HttpContext bağlılığını sınıf seviyesinde tutuyorum ki, olur ya ilgili sınıf altındaki bir davranış daha bu bağımlılığa ihtiyaç duyarsa kullanabilsin diye. Sonra da ilgili bağlılığı fonksiyonum içerisine yediriyorum. Peki testi nasıl oldu bu fonksiyonun?

Gördüğünüz üzere, test ortamında kullanılmak üzere bir HttpContext sınıfı üretebiliyorum artık. Bu sanal sınıfı da, provider sınıfıma parametre geçebiliyorum. Böylece davranışı, gönül rahatlığıyla test edebiliyorum!

“Behind the Scenes”

Bu işin bir de kamera arkası var tabii. Bu işin arkasında “red — green — refactor” denilen teknik var. Kısaca şöyle özetlemek gerekirse;

  • (RED) Olmayan sınıfın instance’ını oluştur — 26. satır —
  • (RED) Olmayan sınıfın olmayan fonksiyonunu çağır — 27. satır —
  • (RED) Olmayan fonksiyonun olmayan geri dönüş değerini teste tabi tut — 28. satır —
  • Build hatalarını düzelt — olmayan sınıf ve fonksiyonları oluştur — ve ilk RED aşamasını tamamla.
  • (RED) Testleri çalıştır ve fail olmalarını izle!
  • (GREEN) Fonksiyona dön ve beklediğin değeri hard-coded olarak geri döndür. Tebrikler! Artık artık testlerin başarıyla geçiyor.
  • (REFACTOR) Şimdi tekrar fonksiyona dön ve kurguladığın algoritmalar ile fonksiyonunu refactor et.

“Director’s Cut”

Yukarıdaki örnek bir .NET Framework örneğiydi. .NET Core bu konuda sizi “doğruya yönlendirmek” adına tabiri caizse sürüklüyor. Örneğin, fonksiyonlarınız içerisinde HttpContext diye bir sınıf kullanamıyorsunuz, çünkü yok. IHttpContextAccessor sınıfından türeyen HttpContextAccessor sınıfına ihtiyacınız var, bunu da mecburen DI ile ilgili sınıfa iletmeniz gerekiyor; gibi gibi senaryolar ile size yardımcı oluyor.

Hem yukarıdaki örneğin kaynak kodlarına hem de .NET Core örneğine aşağıdaki repository üzerinden ulaşabilirsiniz:

Sonuç

Yazılım geliştirme alışkanlıklarımızı değiştirmek oldukça güç ama unutmayın ki bu alışkanlığı da “edindiniz” ve yeni bir alışkanlıkla değiştirmemeniz için hiçbir neden yok. Üstelik daha değerli ve yararlı bir yöntem olduğunu bilmeniz motivasyon konusunda size yardımcı olacaktır. Değişim sürecine girmeye karar verirseniz, şimdiden başarılar! Bir sonraki yazıya dek, aperitifleriniz bol olsun — buraya kebapçı reklamı alınacaktır — :)