Neden Unit Test Yazmalıyız? [Hands On] .NET Core xUnit ile Unit Testing ve GitHub Actions ile Continuous Integration

Sercan DUMANSIZ
9 min readMay 9, 2020

IT endüstrisinde “değişim” hatta “hızlı değişim” son 10 sene içerisinde kaçınılmaz bir gerçek olduğunu kanıtladı.

Günümüzde hızlı çözümler üretmek ve bu çözümleri en kısa süre içerisinde “deliver” etmek, bir beklenti haline geldiği için bu değişime adapte olmak zorundayız.

https://www.thoughtworks.com/insights/articles/enduring-techniques-technology-radar

Son 10 sene içerisinde endüstri değişime adapte olabilmek için birçok çözüm geliştirmiştir.

Bu yazımda, AGILE IT ana başlığı altında konumlandırabileceğimiz Unit Test konusunu;
Neden Unit Test Yazmalıyız? sorusuna cevap vererek inceleyeceğiz.

Neden Unit Test Yazmalıyız? sorusuna geçmeden önce, Neden Test Yazmalıyız? sorusunu kısaca cevaplayalım.

Neden Test Yazmalıyız?

  • Otomatize edilmiş testler, manual testlerden çok daha ucuz ve çok daha verimlidir. Manual testler ile sürdürülebilir bir sistem kurulamaz.
  • Program üzerinde herhangi bir değişiklik gerçekleştiği zaman, en kısa sürede programımızın gerçekleşen değişiklikten nasıl etkilendiğini anlamamızı sağlar.
  • Yazmış olduğumuz kodun hangi bölümlerinin kontrol edildiğini görmemizi sağladığı için, sistemimizin ne kadar sürdürülebilir ve güvenli olduğunu takip edebiliriz.
  • En kısa sürede ve çalışan sistemimizi etkilemeden hatayı fark etmemizi sağlayarak, çalışan sistemimizin hata alma olasılığını düşürürüz.

Test Piramidi

Test Piramidi’nin temelini oluşturan unit testlerin neden en alt basamakta konumlandırıldığını inceleyecek olursak;

  • Unit testler bir problemi çözmek için gereken en temel kod parçacığını test etmeyi hedefledikleri için, sistem o kod parçacığını kullandığı zaman herhangi bir sorun yaşayacak mı? sorusuna en kısa zaman içerisinde ve koda en yakın seviyede cevap bulmamızı sağlarlar.
  • En temel seviyede farkındalık kazandığımız için, sistemi etkileyecek herhangi bir sorunun en kısa sürede bakımını yapabiliriz.
  • Sorunu en temel seviyede farkedip bakımını yaptığımız için, Test Piramidi’nin üst basamaklarına çıkmadan yani daha maliyetli, daha kompleks ve hataya müdahale etmenin daha yavaş gerçekleşeceği basamaklardan önce sistemimizi daha verimli bir şekilde sürdürülebilir kılarız.

Unit Test

https://martinfowler.com/bliki/UnitTest.html

Martin Fowler’ın unit test ile ilgili makalesinde iki farklı unit test yaklaşımından bahsettiğini görüyoruz.

Sociable Unit Test
Sociable Unit Test kavramına baktığımız zaman, problemi çözmek için gereken en temel kod parçacağını test ederken, bağımlı olduğu diğer kod parçacıklarını da unit test’e dahil ettiğini görüyoruz.

Solitary Unit Test
Solitary Unit Test kavramına baktığımız zaman ise, sadece problemi çözmek için gereken en temel kod parçacığını bağımlı olduğu diğer kod parçacıklarından izole ederek test ettiğini görüyoruz.

Programımıza unit test yazarken tek bir yaklaşımı uygulamanın pratikte çok mümkün olmadığını düşünüyorum. Çoğu zaman iki yaklaşımı da uygulayacağız ancak tanım olarak farklarını bilmemiz önemli.

Unit Test Pratikleri

Problem çözmek için kodlamaya geçmeden önce elimizde yeterince “teknik analiz” var ise, yani kod yazmaya başlamadan önce ne yazacağımızın ve nasıl yazacağımızın üzerine düşündüysek unit test pratiklerini uygulamamız da o kadar kolaylaşacaktır.

Unit test yazarken endüstri standartı haline gelmiş 3A Pattern’ı inceleyelim.

3A Pattern (Arrange, Act, Assert)

Arrange
Test gerçekleştireceğimiz kod parçacığının, test edilirken ki duyacağı ihtiyaçların hazırlanacağı adımdır. Örneğin; test edeceğimiz metot farklı bir sınıfın instance’ına ihtiyaç duyuyor olabilir. Bu instance’ın oluşturulması veya kullanılacak olan ortam değişkenlerinin tanımlanması gibi bir çok örnek verebiliriz. Basitçe ihtiyaçların hazır hale getirilmesi olarak algılayabiliriz.

Act
Testi gerçekleştireceğimiz kodun program üzerinde nasıl kullanılacağını belirlediğimiz adımdır. Örneğin; test edeceğimiz metodun ihtiyaç duyduğu parametreleri verebiliriz ya da ilgili fonksiyon çağrılarını gerçekleştirebiliriz. Kısaca kodun sistem içerisinde nasıl kullanılacağını simüle ettiğimiz adım olarak düşünebiliriz.

Assert
Test ettiğimiz kodun program üzerinde beklentimize uygun olarak çalışıp çalışmadığını kontrol ettiğimiz adımdır. Beklentilerimize istediğimiz yanıt veriliyor ise “Passed” verilmiyor ise “Failed” olarak testimiz sonuçlanır.

Bu adımların herhangi bir sıra ile gerçekleşmesine gerek yoktur. Test’i gerçekleştirmemiz için 3 adımın da tamamlanması yeterlidir. Ben genellikle Arrange, Act ve Assert sırasını takip ediyorum.

Ants Bridge

Değişimin çok hızlı gerçekleştiğinden ve bu değişime adapte olmamız gerektiğinden bahsetmiştik. Her ne kadar unit test yazarak bu adaptasyona uyum sağlamaya adım atsak bile unit testlerimizi otomatize etmeden yani testlerimizi manual olarak çalıştırarak bu hıza adapte olamayız. Bu nedenle unit test yazıyorsak, yazmış olduğumuz testlerin kesinlikle otomatize edilmesi gerektiğini düşünüyorum.

Değişime en iyi şekilde adapte olabilmek için sistemlerimizi olabilidiğince otomatize etmemiz gerekmektedir. Bu neden ile bu yazımda oluşturacağım senaryo da sürdürülebilir bir yazılım için gerekli olan temel adımları Continuous Integration pratiklerini uygulayarak otomatize edeceğiz.

[Hands On] .NET Core xUnit ile Unit Testing ve GitHub Actions ile Continuous Integration

Proje Yapısı
Senaryomuzda kullanacağım “Paranoid” kod isimli uygulamayı tanıyarak başlayalım.

Uygulamam içerisinde Twitter API’ye authenticate olmam gerekiyor. Twitter benden OAuth 1.0a metodunu kullanarak authenticate olmamı bekliyor. Ben de bu ihtiyacı gidermek için uygulamam içerisinde Paranoid.OAuth isimli bir class library oluşturdum. Paranoid.OAuth’un temel görevi Twitter API’a authenticate olmam için gereken OAuth imzasını oluşturmaktır.

xUnit ile bir test projesi oluşturmak için dotnet CLI’ı kullanabilirsiniz.

dotnet new xunit -o Paranoid.OAuth.Tests

Paranoid.OAuth içerisinde bulunan sınıfları test edeceğimiz için oluşturmuş olduğumuz test projesine referans olarak eklememiz gerekiyor.

cd (Change Directory) komutunu çalıştırarak, Paranoid.OAuth.Tests directorysine gidiyoruz.

cd Paranoid.OAuth.Tests

dotnet add reference ../Paranoid.OAuth/Paranoid.OAuth.csproj

Test projemizi hazır hale getirdik.

Unit Test
İlk olarak Solitary Unit Test örneği olarak kullanacağımız UriHelper.cs sınıfını inceleyelim.

GetBaseUrl → URL’i query parametrelerden kurtararak baseURL’i elde etmemizi sağlar.

GetQueryParameters → URL içerisinde bulunan query parametreleri key, value şeklinde ayırmamızı sağlar.

(TestEdilenMetot_TestEdilenDurum_BeklenilenDavranış) isimlendirme formatını kullanılarak oluşturduğum unit test metotları.

UriHelper sınıfı static bir sınıf olduğu için, Arrange adımında herhangi bir instance oluşturma vs. ihtiyacı duymuyoruz.

Act adımında ise UriHelper sınıfına ait GetBaseUrl metodunu direkt olarak kullanabiliyorum. Bu kısımda GetBaseUrl metodu string url parametresi beklemektedir. xUnit’in sağlamış olduğu Theory ve InlineData attribute’lerini kullanarak test metoduma test senaryom için belirlediğim parametreleri gönderebiliyorum.

Assert adımında ise xUnit’in Assert sınıfı altında bulunan Equal metodu ile InlineData’da belirlemiş olduğum expected değerinin GetBaseUrl metodundan dönen değerle aynı olmasını beklediğimi belirtiyorum.

Aynı yöntemi GetQueryParameters metodunun unit testini yazmak için de kullanıp herhangi bir farklı iş yapan metodların dahil olmadığı, sadece ilgili metodların kullanıldığı Solitary Unit Test’lerimi hazır hale getiriyorum.

Bu yazımda unit test nasıl yazılır? sorusuyla çok fazla ilgilenmiyoruz, temel amaç sürdürülebilir yazılım pratiklerini anlatmak. Sizlerin merak ettiği detaylar var ise, yorumlar kısmında sorularınızı sorabilir unit test ile ilgili tartışmak istediğiniz konuları tartışabiliriz.

İkinci olarak ise Sociable Unit Test örneği için kullanacağımız OAuthHeaderGenerator.cs sınıfı içerisinde bulunan GenerateOAuthHeader metodunu inceleyelim.

Metoda baktığımız zaman tek bir iş yapıyor olsa bile bu işi yapmak için farklı metodları kullanması gerekiyor. Bu metodun test sonucunda beklenen karşılanıyor ise, bu metod içerisinde kullanılan diğer metodların da en azından bu metodun işini yapması için gereken beklentiyi karşıladıklarını söyleyebiliriz.

Bu test metodunu incelediğimiz zaman Arrange adımında bir kaç hazırlık yapmamız gerekiyor, çünkü sınıfımız constructor’ında bir kaç parametre bekliyor. O parametreleri tanımladıktan sonra unit testimi yukarıda ki diğer örneklerde kullandığım pratikleri kullanarak tamamlıyorum.

Artık CLI üzerinden dotnet test komutunu çalıştırarak unit testlerin sonuçlarına bakabilirim.

Yazmış olduğum unit testler sayesinde Class Library’i herhangi bir execute edilmesi gereken programa ihtiyaç duymadan test etmiş oldum. Kullandığınız IDE üzerinden de gereken durumlarda unit testlerinizi debug edebilirsiniz.

Unit test yazmanın, kodumuzun çalıştığını anlamak için uygulayacağımız diğer yöntemlerden çok daha verimli ve hızlı olduğunu düşünüyorum. Ayrıca test edilebilir kod yazmak, ister istemez kod kalitenizi ve standartlara uyma zorunluluğunuzu arttıracaktır. Unutmayın ki yazmış olduğunuz unit testler programınız yaşadığı süre boyunca işlevlerini yerini getireceklerdir.

Unit testlerimizi yazdıktan sonra, sıra geldi onları otomatize etmeye. Yukarıda bahsettiğim gibi eğer test yazıyorsanız benim yaklaşımıma göre onları kesinlikle otomatize etmelisiniz. Unit testlerin gerçek işlevselliklerine otomatize edildikleri zaman ulaştığını düşünüyorum.

Continuous Integration senaryomuz ile devam edelim.

CI sürecimizi GitHub üzerinde bulunan GitHub Actions ile gerçekleştireceğiz.

Developer geliştirmekte olduğu yeni feature’u tamamladıktan sonra development brach’ine merge edilmesi için Pull Request oluşturur ve senaryomuz başlar.

Pull Request oluşturulduğu an GitHub Actions üzerinde oluşturacağımız Workflow trigger olur ve belirlemiş olduğumuz adımları gerçekleştirir.

Senaryomuzda restore build gibi işlemleri ayırmayacağım dotnet test komutu öncelikle restore sonra build işlemini gerçekleştirmektedir ancak siz bu adımları ayrı yönetmek isterseniz bu adımlarıda Workflow üzerinde ayrıca tanımlayabilirsiniz.

Eğer unit testlerimiz istediğimiz şekilde tamamlanırsa, Code Coverage raporumuzu oluşturarak Workflow tamamlanıyor, eğer unit testlerimiz fail ederse Workflow bir sonraki adımları gerçekleştirmez ve sonlanır.

line,branch ve method coverage için coverlet msbuild kullanacağım.
rapor oluşturmak için ise report generator kullanacağım.

<ItemGroup>
<DotNetCliToolReference Include=”dotnet-reportgenerator-cli” Version=”4.5.8" />
</ItemGroup>
<PackageReference Include=”ReportGenerator” Version=”4.5.8" /><PackageReference Include=”coverlet.msbuild” Version=”2.8.1" />

Workflow’un doğru çalışması için test projenizde bulunması gereken paketler.

Workflow’umuzu oluşturarak başlayalım.

GitHub repository’inizde bulunan kod’a göre Workflow önerilerinde bulunacaktır. Ben empty bir template ile ilerleyeceğim. Set up workflow yourself → diyerek devam ediyorum.

Sıfırdan bir template oluşturmama rağmen GitHub belirli bir örnek içeren Workflow ile sizleri karşılıyor ben bu adımda sadece Workflow ismini PR-development.yml olarak güncelleyerek bu dosyayı master branch’ine ekliyorum.
GitHub Workflow’u güncellemeniz için ve Marketplace’de search yapmanız için bir arayüz sunuyor. Ben bu arayüzü kullanmadan Visual Studio Code üzerinden devam edeceğim.

Senaryomuz da development branch’ine pull request açıldığı zaman Workflow’un çalışmasını istediğimiz için öncelikle master branchinden development branch’i git checkout developmentoluşturuyorum ve bu branch’e pull request isteği gönderecek olan feature branch’i git checkout feature/demo isimli branchleri oluşturuyorum.

demo branch’imde ufak bir değişiklik gerçekleştirip development branch’ine pull request oluşturuyorum.

Pull Request’imiz oluşturulduktan sonra, Workflow belirlemiş olduğumuz tüm adımları tamamlayarak işlemini sonlandırıyor ve son adımda istemiş olduğumuz code coverage raporunu oluşturuyor.

Paranoid.OAuth.Tests projesinde yazmış olduğum testlerin Line Coverage ve Branch Coverage detayları.

Rapor sonucuna baktığımız zaman OAuthHeader modelimde %55 Line Coverage’ı sonucu alıyorum detayına baktığım zaman ise kullanmadığım alanlar olduğunu görüyoruz.

Bu sonucu düzeltmek istediğim için, demo branch üzerinde bir güncelleme yaparak tekrar Workflow’umun çalışmasını istiyorum. (Pull Request açık olduğu süre boyunca PR açılan branch’de yapılan değişiklikleri PR üzerine yansıtır.)

İstemiş olduğum gibi gerekli geliştirmeyi yaptıktan sonra, Line Coverage sonucumun da bu class özelinde %100'e ulaştırınca senaryomu tamamlıyorum.

[Hands Off]

Senaryomuzu tamamladık. [Hands On] bölümü ile alakalı soru veya görüşlerinizi yorum olarak iletebilirsiniz.

PHOTOGRAPH BY JOEL BEAR, NATIONAL GEOGRAPHIC YOUR SHOT

Neden Unit Test Yazmalıyız? sorusunu sürdürülebilir yazılım geliştirme pratiklerini uygulayarak anlatmak istedim. Unit test yazmak, sürdürülebilir yazılım geliştirme ortamı için en alt seviyeden, en üst seviyeye kadar etki eden bir pratiktir. Biz örneğimizde yazılım geliştirici tarafından bir örnek ele aldık ancak unit test yazmanın proje, takım hatta şirket özeline kadar etki ettiğini söylemekte fayda var. Benim kendi görüşüme göre sürdürülebilir sistemler bireylere bağımlı olmamadırlar. Sürdürülebilir yazılım geliştirme pratiklerinin en temelinde yer aldığını düşündüğüm unit test yazmak, bağımlılıkların gevşetilmesinde işlevsel bir rol oynamaktadırlar.

Temel anlamda test etmek, kontrol etmek hayatımızın birçok alanında karşımıza çıkmaktadır. Eğer “kaliteli” bir yazılım geliştirme ortamına sahip olmak istiyorsak sık test etmeyi, sık kontrol etmeyi ve her zaman güncel kalmayı bir alışkanlık haline getirmeliyiz.

Referanslar

--

--