Birim Testleri
Unit Test
1. Amaç
Bu belgenin amacı .NET uygulamalarında geliştirilen birim testlerine ait pratikleri tanıtmaktır.
Kaynaklar
2. Giriş
Bir birim testi, bir kod biriminin (genellikle bir nesnenin veya modülün belli bir metodunun) davranışlarını test eden ve doğrulayan özel bir metottur. Bu kod biriminin olası tüm davranışları için testler oluşturulduğunda, birimin sistemin diğer bölümlerinden bağımsız bir biçimde doğru çıktıları sağladığı doğrulanmış olur.
Birim testleri, uygulama değiştirildiğinde, test ettikleri birimlerin işlevlerini doğru yerine getirmeye devam edip etmediğini kontrol etmeyi sağlarlar. Ayrıca test edilen metotların çalışma mantıklarını da belgelemeye yararlar.
Birim testleri olan uygulamalarda Refactoring - Yeniden Düzenleme teknikleri güvenli bir şekilde uygulanabilir. İşlevlerin doğru çalıştığından emin olunarak yazılımın yapısı istendiği gibi yeniden düzenlenerek kod kalitesi ve organizasyonu iyileştirilebilir.
2.1 Test Türleri
Birim testleri bir sistemin test edilme yöntemlerinden sadece birisidir.
Unit Testing
Birim Testleri
Whitebox Testing
Integration Testing
Entegrasyon Testleri
System Testing
Sistem Testleri
Acceptance / Functional Testing
Kabul Testleri
Blackbox Testing
2.2 Yaklaşımlar
Name
Value
Birim testleri
→ ne yapılacağını belirler.
Test Driven Development (Test Güdümlü Programlama)
→ testin ne zaman yazılacağını belirler.
Behavior Driven Development (Davranış Güdümlü Programlama)
→ testin nasıl yazılacağını belirler.
Her biri ayrı ayrı da kullanılabilse de, en iyi sonuçlar için bu yöntemler bir arada kullanılmalıdır. Zaten beraber uygulanmaya da çok uygundurlar.
TDD (Test Driven Development) Test Güdümlü Programlama: İşlevin geliştirilmesinin önce testlerinin yazılarak sağlandığı yöntem. Bkz. Test Driven Development (TDD)
BDD (Behaviour Driven Development) Sistemin davranışlarına odaklanan bir geliştirme yöntemidir. Bu yaklaşımda testler implementasyon yerine beklenen davranışı (fonksiyonalite) test etmeye yoğunlaşır. Bkz. Behaviour Driven Development (BDD)
Kullanılan terminoloji iş beklentilerini karşılamaya yönelik oluşturulur ve iş uzmanları ile geliştiriciler arasında oluşan iletişim boşluğunu da gidermeyi hedefler.
Tasarımı, beklenen davranış üzerinden giderek düşünürsek ve organize edersek, iş ihtiyaçlarının mantıksal yerleşimi testlere yansır, test sınıfları fonksiyonaliteyi test eder hale gelir.
3. Bir Testin Anatomisi
3.1 Örnek Birim Testi
3.2 Bir Birim Testinin Organizasyonu Hakkında Öneri: A/A/A
Bir birim testi 3 temel bölümden oluşur. Bu bölümler yorum ifadeleriyle birbirinden ayrılabilir.
Name
Value
Arrange
Test edilecek işlevin çalıştırılabilmesi için gerekli kurulumlar.
Act
İşlevin çalıştırıldığı bölüm.
Assert
Sonucun kontrolü.
Arrange Bu bölümde test edilecek birime, koşullara ve sonuçlara ait kurulumlar yapılır.
Setup
Bir metodun belli parametrelerle çalıştırılması sonucunda döndürdüğü sonucu belirlemek için kullanılır.
Act Bu bölümde test edilecek birim istenen koşullarla çalıştırılır ve sonuç elde edilir.
Assert Bu bölümde çalıştırılan birimin ürettiği sonuç kontrol edilerek beklentileri karşılaması sağlanır. Testin geçip geçmediği bu bölümde belli olur. Bu nedenle beklenen sonuçlar için kontrol ifadeleri kapsamlı bir şekilde tanımlanmalıdır.
ASP.NET MVC yapısı kullanılıyorsa ve test edilecek birim Controller katmanında ise işlem sonucunda arayüzle ilgili sonuçlar kontrol edilir.
Verify
Bir metodun çalışıp çalışmadığını kontrol etmek için Verify
metodu kullanılır.
Bir sınıfa ait özelliğe atama yapılıp yapılmadığı SetValue
metodu ile kontrol edilir.
Mock kurulumu yapılan bir metot sonuç döndürüp akışın sürmesini sağlıyorsa, kurulum yapılan metodun çalıştığını Verify
etmeye gerek kalmaz.
3.3 SetUp / TearDown metotları
SetUp
Bir test sınıfının içerdiği tüm testlerde kullanılacak kurulumlar için SetUp
metodu kullanılır.
SetUp
metodu her bir test çalışmaya başlamadan önce çalışır.
Bu metotta genelde test edilecek sınıfa kullanacağı arayüzlerin Mock versiyonları enjekte edilir. Böylece bu arayüzlerin istenen metotlarının mock kurulumları ile istendiği gibi sonuç üretmesi sağlanabilir.
TearDown
Bir test sınıfının içerdiği tüm testlerde kullanılacak sonlandırma işlemleri için TearDown
metodu kullanılır.
TearDown
metodu her bir testin çalışması tamamlandıktan sonra çalışır.
4. Birim Testlerinin İsimlendirilmesi
bkz. Birim Testi İsimlendirme Kılavuzu
5. Test Geliştirme Pratikleri
5.1 Prensipler
bkz. Birim Testi Geliştirme Prensipleri
5.2 Testleri Paralel Çalıştırmak
Testlerin hızlı çalışması önemlidir, çünkü testler hızlı çalışmıyorsa, çalıştırılmamaya başlanır. Testler sık çalıştırılmazsa refactoring yapma eğilimi de azalır (çünkü programcının kendine olan güveni azalır).
Birim testlerini paralel çalıştırmak zamandan kazandırır. Testleri paralel çalıştırmak için NUnit 3.0 ve üstü versiyonlarda test sınıflarına (TestFixture) [Parallelizable(ParallelScope.Fixtures)]
etiketi eklenir.
Vaka: Foo projesinde test sayısı 800’ü geçince testlerin çalışma süresi 10 saniyeye çıktı. Paralel çalıştırma sonucunda 2.600 civarında test ortalama 6,50 saniye gibi bir sürede çalışabiliyor.
6. Birim Testlerinde Sorunlar ve Çözümler
6.1 Test Yazmanın Uzun Sürmesinden Yakınan Geliştiriciler İçin Açıklamalar
1. Arrange bölümü çok uzun
Önce Act kısmını yaz, ihtiyacın kadar Setup ekle. (Test edilen metodu olabildiğince Black Box olarak düşün. Bu şekilde düşünmek metodun soyutlama seviyesini belirlemekte de yararlıdır.)
İhtiyaç olmadığı sürece, Mock sınıfların Setup tanımlarına verilen parametre ve dönüş tipi sınıflarının özelliklerine atama yapma ya da sadece yapılması gerektiği kadar yap. (Kurulumların çalışması için bu sınıfları sadece new ile oluşturmak ve nesnenin referansını kullanmak yeterlidir.)
Birden fazla testte kullanılan setup bölümleri ayrı bir metoda alınıp ortak olarak kullanılabilir. (Parametreler için factory metotlar oluşturulabilir.)
Yine de çok fazla metodu setup yapmak gerekiyorsa bu durum, test edilecek metodun birden fazla sorumluluğu yerine getirmeye çalıştığını gösterir. Bu sorumlulukların soyutlama seviyeleri büyük olasılıkla farklıdır. Alt seviye sorumlulukları yerine getiren adımlar gruplanıp başka bir sınıfa sorumluluk olarak taşınabilir. Bu durumda taşınan metodun setup'ını yapmak yeterli olur. İşler birden fazla sınıfa dağılacak şekilde anlamsal bir bütünlüğe sahipse böyle yap.
2. Assert satırları fazla
FluentAssertions veya benzeri, BDD (Behaviour-Driven Development) odaklı, zincir metotlar ile çalışabilen bir Assert kütüphanesi kullan.
Assert bölümü testin amacıdır, en önemli kısmıdır.
Testin neden yazıldığı, hangi sonuçların beklendiği açık, anlaşılır ve kısa olmalı.
FluentAssertions ile cümle okur gibi okunabilen, BDD odaklı, daha az satırdan oluşan Assert ifadeleri yazılabiliyor.
3. Çok fazla test oluşturmam gerekiyor
Sadece belirli koşulları ya da sonuçları değişen testleri yeniden yazma. Parametrik testler oluşturarak, değişen bölümleri parametre olarak tanımla. (TestCase kullan.)
6.2 Sık Sorulan Sorular
1. Setup çalışmıyor
Setup'a sağlanan parametreyi kontrol et. Parametrenin test edilen birimde (metotta)
Değer tipi parametre ise aynı değerle
Referans tipi ise aynı referansla çalıştırıldığından emin ol.
Aynı değilse setup başarısız olur, test de (çoğu zaman) geçmez.
Setup’a sağlanan referans tipi parametre, test edilen metodun içinde farklı bir referansla oluşturuluyorsa (örneğin
new
anahtar kelimesi ile) setup başarısız olur. Bu durumda yaşanan genel yanılgı şudur: “Parametre sınıfı için tüm özellikleri beklenen değerlere sahip bir nesne oluşturmak bu işi çözebilir.” Hayır, çözmez. Sınıfın özelliklerine ait değerler aynı bile olsa referansı farklı olacaktır. Bu durumun olası çözümleri:Test edilen metodun oluşturduğu ve kullandığı nesne için bir factory metot oluşturulur. Metot nesneyi factory aracılığıyla oluşturur ve kullanır. Testte factory metodunun kurulumu yapılır ve kullanılacak parametre bu metodun dönüşünde sağlanır. İmplementasyon ile test aynı yöntemi kullanarak parametreyi oluşturacağından setup çalışır.
Setup metoduna bir nesne vermek yerine
It.IsAny<ParameterType>()
ifadesi ile parametre sağlamak. Bu kuruluma göre aynı tipte herhangi bir nesne parametre olarak kullanılırsa, setup başarılı olur. (Bu kullanım önerilmez. Bkz. Not - It.IsAny Kullanımı)
Not - It.IsAny Kullanımı
Setup yapılan metotlarda parametreler için It.IsAny
tipini kullanmak sakıncalıdır.
SORUN 1: Metot içindeki çalışma akışında parametre için doğru nesne kullanıldığından emin olunamaz. Test fonksiyonaliteyi tam anlamıyla test ediyor olmaz.
SORUN 2:
It.IsAny
kullanmak gerekmesi bir soruna işarettir. İmplementasyon içindenew
anahtar kelimesi ile sınıf oluşturulduğunu gösterir.new
ile sınıf ancak Factory metotlarda oluşturulmalı. Bu durumlarda Factory metot kullanılmalıdır. Yeni oluşturulan sınıfın istenen değerlerle oluşturulup oluşturulmadığı (verilen değerler ve beklenen varsayılan kurulumlarıyla) Factory metodun testinde kontrol edilir.
2. .NET Framework veya 3. parti bir bileşene ait metot beklendiği gibi çalışmıyor
Url
,ControllerContext
,HttpContext
,File
,Session
, vb. Bileşenler birim testi ortamında beklendiği gibi çalışmaz. (Özellikle birHttpContext
nesnesine ihtiyaç duyan Web metotları)Bu durumlarda gevşek-eşleştirme (loose-coupling) sağlanmalı. Wrapper sınıflar oluşturulmalı ve istemci sınıf bu framework bileşenlerine wrapper sınıfların arayüzleri aracılığıyla ulaşmalıdır.
Wrapper metotlar istemcinin çağıracağı hedef metot ile aynı imzaya sahip vekil metotlardır.
Wrapper → Kullanılan bileşenin metotları ile aynı imzaya sahip arayüz oluşturulur, bu arayüzü uygulayan wrapper sınıfı, metotların implementasyonlarında hedef bileşenin metotlarını çalıştırır ve sonucu döndürür.)
Örnek:
ISession
(arayüz)SessionWrapper
(implementasyon)RedisSessionWrapper
(implementasyon)
Sıkı sıkıya eşleştirmekten kurtulmanın faydası: Örneğin
Session
işlemleri için kullanılanISession
arayüzünün kullanılan implementasyonu istendiği zaman kolayca değiştirilebilir. ASP.NETSession
nesnesini saranSessionWrapper
yerine Redis kullananRedisSessionWrapper
kullanımına geçilebilir ve istemci kodu ve testleri bu durumda değişmek zorunda kalmaz.
6.3 Birim Testleri ve Teknik Olmayan Proje Paydaşları
Geliştirme sürecinde birim testi yazıldığını müşteriye, proje yöneticisine bildirmeyin. Testleri geliştirmenin doğal bir parçası olarak düşünün. Yeterli bilgiye sahip olmayan kişiler, test yazmanın geliştirmeyi uzatacağı, gereksiz olduğu gibi önyargılara sahip olabilir (genelde böyle olur). Dikkate almayın. Testlerin geliştirme sürecinin doğal ve gerekli bir aşaması olduğunu içselleştirememiş organizasyonlarda, bunu açıkça belirtmeden testlerinizi yazmanız daha iyi olabilir.
Öneri: Test Driven Development (TDD) yöntemi ile geliştirme yaparsanız, işlevin kodlamasını bitirmiş ama testlerini henüz yazmamış bir durumda kalmazsınız. Önce testi yazarsınız ve uygulama kodu da doğal olarak oluşturulmuş olur. Geliştirme bittiğinde testler de, uygulama kodu da tamamlanmış olur.
7. Test Assert Örnekleri
Bu bölümde gösterilen örneklerde FluentAssertions
kütüphanesi kullanılmıştır.
7.1 Web Testleri
Yönlendirme Testi
ViewResult ve Model Testi
JsonResult ve Data Testi
TempData ve ViewData Testi
ASP.NET MVC ModelState Kontrolü
Controller testlerinde ModelState
nesnesinin geçersiz olduğu, hata içerdiği durumları kontrol etmek için aşağıdaki Assert ifadesi kullanılabilir.
GetErrorCount
metodu ModelState
sınıfı için oluşturulan bir extension metottur:
Model validasyonu testler çalışırken devreye girmediğinden ModelState
nesnesini hatalı duruma getirmek ve bu durumu test etmek gerekebilir. Bu durumda ModelState
hatasını elle oluşturmak amacıyla Act bölümünden önce, test edilecek Controller için AddModelError
metodu kullanılabilir.
7.2 Validasyon Testleri
FluentValidation Testleri FluentValidation testleri genelde sadece Assert bölümlerinden oluşan kısa testlerdir. Bu testlerde birden fazla durum aynı koşul için aynı metotta test edilebilir.
Validasyon testlerinin aşağıdaki gibi iki bölüme ayırılması testlerin organizasyonu açısından yararlıdır.
Invalid conditions
Valid conditions
Validasyon testlerinde validasyonu ihlal eden koşullar kontrol edildiği gibi, kabul edilebilir değerlerin de bir ihlale yol açmadığı da kontrol edilmelidir.
Diğer durumlarda kullanılabilecek test isimleri:
Validate_ValueNotInRange_HaveError
Validate_ValueLengthNotInRange_HaveError
Validate_ValueFormatInvalid_HaveError
Validate_ValuesDoNotMatch_HaveError
Özel kontroller için
Validate_CityEmpty_HaveError
Validate_AddressValueMaxLength_HaveError
7.3 Hata (Exception) Testleri
Test edilecek işlev bir sonuç döndürmek yerine hata fırlatıyorsa Assert.That
metodu ile beklenen hata tipi belirtilir ve bu hatanın fırlatılması durumunda test geçmiş olur. Metodun çağrısı ile sonucun kontrolü aynı satırda olduğu için Act ve Assert bölümleri birleşir.
Akışın belli bir aşamasında bir metodun hata fırlatması istenirse, bu metodun Setup işlemi Returns yerine Throws metodu ile yapılır.
8. Yardımcı Kütüphaneler
NUnit
.NET için açık kaynaklı bir birim testi kütüphanesi.
https://www.nuget.org/packages/NUnit
Moq (Mocking library)
Mocking, birim testlerinde gerçek uygulama sınıfları yerine bu sınıfların özelliklerine sahip (aynı arayüzleri uygulayan) geçici sınıflar tanımlama ve kullanma yöntemi.
Moq, bir arayüzden mock nesnesi yaratıldığında, çalışma zamanında arayüzü uygulayan bir sınıf oluşturarak bu sınıfın kullanılmasını, bu sınıfın özelliklerinin ve metotlarının beklenen sonuçları verecek şekilde ayarlanmasını sağlar.
https://www.nuget.org/packages/Moq/ http://code.google.com/p/moq/
FluentAssertions
Daha önce kullanılan basit UnitTestHelper
sınıfındaki ShouldEqual
, ShouldNotEqual
metotları yerine çok daha kapsamlı olan ve BDD yönelimli FluentAssertions
kütüphanesinin kullanımına geçildi.
FluentAssertions Yararları:
Daha anlamlı, cümle yapısına sahip
Daha kısa Assert ifadeleri yazılabiliyor
Test satır sayısı da azalıyor
Assert ifadelerindeki because parametresi aslında ne beklendiğini ve gerekçesini, geçmeyen testlerin çıktılarına daha anlaşılır bir şekilde yansıtmayı sağlıyor.
TestDriven.Net
VisualStudio ile NUnit’in entegrasyonunu sağlayan, testi debug ile çalıştırmaya ve NCover programı ile coverage gösterebilen eklenti.
Visual Studio entegrasyonu
https://www.nuget.org/packages/NUnitTDNet
NCrunch (Automated Concurrent Testing Tool)
Ninject (Dependency injection library)
Ninject, sınıfların bağımlılıklarını ayarlamaya yarayan bir kütüphane. Arayüzlerle bağlanan sınıfların çalışma zamanında hangi somut sınıflarla çalışacağının belirlenmesini sağlar.
Hangi controller’ın hangi somut sınıfların nesnelerini kullanacağını belirlemek için Ninject kullanılan özel ControllerFactory
sınıfı, uygulamanın kullandığı ControllerFactory
olarak Global.asax
içinde Application_Start
olayında atanır.
Test için FluentAssertions.Ioc.Ninject paketi
Last updated
Was this helpful?