Unit Test Yazarken Pratik Mocklama

Merhaba arkadaşlar.

Bir süredir architectural düzeyde devam ettirmeye çalıştığım yazı serilerimi, bu aralar vakit buldukça biraz daha test ağırlıklı konulara kaydırmaya karar vermedim. Çünkü birisi henüz yazılım projesi inşa aşamasında iken kaliteli bir şekilde altyapı üzerine kurulması ile ilgilenirken, bir diğeri ise hızla gelişen ve büyüyen projenin kod kalitesinin sürekliliği ile ilgilenmektedir değil mi?

İşte bu noktada iki uç arasında bir şekilde dengeyi sağlayamayarak hep ipin ucunu kaçırıyoruz/kaçıyor. Her ne kadar projenin mimari altyapısına önem veriyorsak, aslında aynı eforu bu kalitenin sürekliliğini sağlayabilmek adına test senaryolarında da göstermeliyiz. (Tabi bu Türkiye koşulları göz önüne alındığında, zaman/maliyet oranına göre her ne kadar teste önem verildiği tartışılır.)

Her neyse test konusunun önemini kısaca bir kere daha hatırlatma isteğimden sonra konumuza yavaşça giriş yapabiliriz. 🙂

Unit test yazarken kolay mock işlemlerimizi yapabilmemizi sağlayan NSubstitute Framework‘ünden bahsedeceğim bu yazımda. Bu framework’den bahsetmeden önce kısaca Mock nedir ve neden ihtiyaç duyarız’ı bir hatırlamamızda fayda olacağını düşünüyorum.

Mock/Mocklamak Nedir?

Mock kavramı istediğimiz bir objenin yerine geçebilen fake objelerdir. Bu objelerin istediğimiz gibi davranmalarını sağlayabiliriz.

Mock/Mocklamak Bize Ne Sağlar?

  • Unit test bir birimi test ettiği için, oradaki akışı test ederken bu akışa bağlı olan dependency‘lerin test akışını bozmamasını sağlar.
  • Unit test işlemini yaparken, test’i istediğimiz senaryoda yönlendirebilmemizi sağlar.
  • Complex objelerin yavaşlıklarından kurtulabilmemizi sağlar.

Kısaca Mock kavramından bahsettiğimize göre artık yavaşça kodlamaya başlayabiliriz. Haydi bakalım! 🙂

Not: NSubstitute framework’ü open-source’dur. Buradan ilgili repository’sine ulaşabilirsiniz.

Öncelikle Test Case’lerimizi yazabilmek için bir Unit Test projesi oluşturalım. Oluşturmuş olduğumuz bu projeye “Nuget Package Manager” üzerinden “NSubstitute” yazarak, aşağı görseldeki framework’ü kuralım.

Gerekli kurulumu yaptıktan sonra test işlemleri için basit bir senaryo hazırlayalım. Varsayalım ki bir e-ticaret sitesi geliştiriyoruz ve ürün stok işlemlerini yapabilmemiz için bir servisimiz bulunmakta. Bizim için kritik olan bu servis için ilgili business logic’inin bozulmadığını doğrulayabilmek adına Unit Test yazmak istiyoruz. Bu senaryomuzu uyarlamak adına basit bir şekilde Test Driven Development‘a uygun olarak hazırlayalım.

Not: Test işlemlerimizi tam anlamıyla gerçekleyebilmemiz için kesinlikle projelerimizin Test Driven Development’a ve SOLID prensiplerine uygun bir şekilde geliştirilmiş olup, abstraction’ların doğru uygulanması gerekmektedir.

NSubstitute ile Mock Oluşturma

NSubstitute Framework’ü ile kolaylıkla Mock oluşturabilmek için aşağıdaki syntax’ı kullanmamız yeterlidir.

var mockObject = Substitute.For<T>();

Bu işlem sonucunda ilgili T tipimiz için bir Mock işlemini başlatacağımızı söylüyoruz ve bağımlılığı inject ediyoruz. Daha sonrasında ise bu bağımlılığın hangi method için nasıl davranması gerektiğini, unit test senaryolarımız doğrultusunda aşağıdaki syntax ile kolaylıkla belirleyebiliyoruz.

var mockObject = Substitute.For<T>();

mockObject.Sum(Arg.Any<int>(),Arg.Any<int>());

Burada mockObject’in “T” tipinden “Sum” method’unun int tipinde iki parametre alacağını belirtiyoruz. Dilersek de bu iki parametre sonucunda yapması gereken davranışı da söyleyebiliriz!

var mockObject = Substitute.For<T>();

mockObject.Sum(Arg.Any<int>(),Arg.Any<int>())
.Returns(10);

“Sum” method’unun işlem sonucunda ise “Returns” extension’ı ile geriye “10” değerini dönmesi gerektiğini söylüyoruz. İşte hepsi bu kadar.

Örnek Bir Unit Test Senaryosu ve Mock İşlemi

Şimdi örnek projemiz için öncelikle entity’leri tanımlayalım:

namespace UnitTestMockingSample
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Stock { get; set; }
    }
}

Product entity’si basit bir şekilde senaryo gereği sadece Id, Name ve Stock property’lerine sahip olacaktır.

Şimdi contract’larımız niteliğinde olan interface’lerimizi tanımlayalım:

namespace UnitTestMockingSample
{
    public interface IProductRepository
    {
        Product GetById(int productId);
    }
}

IProductRepotistory Product işlemlerimizi gerçekleştirmemizi sağlayan repository contract’ımız niteliğindedir ve içerisinde sadece “GetById” method’unu barındırmaktadır.

namespace UnitTestMockingSample
{
    public interface IStockRepository
    {
        bool ChangeStock(Product product, int stock);
    }
}

IStockRepository ise, ilgili stock işlemlerini farklı bir tabloda gerçekleştirecek olan repository contract’ımızdır. “ChangeStock” method’u ile stock işlemlerini gerçekleştirecektir.

namespace UnitTestMockingSample
{
    public interface ILogger
    {
        void Log(string message);
    }
}

ILogger ise, olmazsa olmazımızdır. Her bir exception’u “Log” method’u ile bir yerlerde kaydetmekteyiz. 🙂

Şimdi geldik şu meşhur ürün stok işlemlerini gerçekleştirecek olan servisi tanımlamaya.

using System;

namespace UnitTestMockingSample.Business
{
    public class ProductStockService
    {
        private readonly IProductRepository _productRepository;
        private readonly IStockRepository _stockRepository;
        private readonly ILogger _logger;

        public ProductStockService(IProductRepository productRepository, IStockRepository stockRepository, ILogger logger)
        {
            _productRepository = productRepository;
            _stockRepository = stockRepository;
            _logger = logger;
        }

        public bool ChangeStock(int productId, int stock)
        {
            Product product;

            try
            {
                product = _productRepository.GetById(productId);
            }
            catch (Exception ex)
            {
                _logger.Log(ex.ToString());

                return false;
            }

            if (product != null)
            {
                // bla bla stock işlem business logic'leri...

                return _stockRepository.ChangeStock(product, stock);
            }
            else
            {
                _logger.Log("Stok bilgisi değiştirilemedi.");

                return false;
            }
        }
    }
}

ProductStockService class’ı ise basit bir şekilde constructor aracılığı ile inject ettiği bağımlılıkları kullanarak, “ChangeStock” method’u ile ilgili stock işlemlerini gerçekleştirmektedir.

Dikkat ederseniz Mock’un faydalarını hatırlarken bahsetmiş olduğumuz; “Unit test bir birimi test ettiği için, oradaki akışı test ederken bu akışa bağlı olan dependency‘lerin test akışını bozmamasını sağlar.” kısmına bir kez daha dikkat çekmek isterim. Bizim bu servis üzerindeki yapmak istediğimiz işlem “ChangeStock” method’unun ilgili stok değiştirme business logic’inin düzgün çalışıp çalışmadığıdır aslında. Bizi ne o an “_productRepository” üzerinden ürünün gelip gelmediği ilgilendiriyor, nede “_stockRepository” içerisindeki işlem. İşte tam burada Mock objeler imdadımıza koşuyor! Gerçekte yoklar, ama aslında varlar gibi. 🙂 Buradaki amacımız işin özünde bu dış bağımlılıklarımızdan kurtularak, asıl hedefimiz düzgün çalışabilmesidir.

Haydi şimdi mock’layarak, test senaryolarımızı oluşturalım.

using Microsoft.VisualStudio.TestTools.UnitTesting;
using NSubstitute;
using UnitTestMockingSample.Business;

namespace UnitTestMockingSample
{
    [TestClass]
    public class ProductStockServiceTests
    {
        private IProductRepository _productRepository;
        private IStockRepository _stockRepository;
        private ILogger _logger;

        private ProductStockService _productStockService;

        [TestInitialize]
        public void Initialize()
        {
            _productRepository = Substitute.For<IProductRepository>();
            _stockRepository = Substitute.For<IStockRepository>();
            _logger = Substitute.For<ILogger>();

            _productStockService = new ProductStockService(_productRepository, _stockRepository, _logger);
        }

        [TestMethod]
        public void ChangeStock_WhenProductNotNull_Change()
        {
            // Arrange
            _productRepository.GetById(Arg.Is<int>(a => a < 10))
                .Returns(new Product() { Name = "Asus PDA", Stock = 100});

            _stockRepository.ChangeStock(Arg.Any<Product>(), Arg.Any<int>())
                .Returns(true);

            // Act
            var result = _productStockService.ChangeStock(5, 50);

            // Assert
            Assert.IsTrue(result);
        }

        [TestMethod]
        public void ChangeStock_WhenProductNull_WriteLogMessage()
        {
            // Act
            _productStockService.ChangeStock(5, 50);

            // Assert
            _logger.Received().Log(Arg.Any<string>());
        }

        [TestCleanup]
        public void Cleanup()
        {
            _productRepository = null;
            _stockRepository = null;
            _logger = null;
        }
    }
}

ProductStockServiceTests class’ı çatısına baktığımızda, klasik olarak tüm testlerimiz “Initialize” ile başlayıp, “Cleanup” method’u ile son buluyor.  Test yazma logic’imizde ise yine standartlar doğrultusunda gidebilmek için AAA (Arrange, Act, Assert) pattern’ini uyguluyoruz. Detaylı bilgi için buradan ulaşabilirsiniz. Şimdi gelelim bakalım neler yaptık?

Tüm interface’lerimizi global olarak tanımlayarak, “Initialize” method’u içerisinde bahsetmiş olduğumuz:

Substitute.For<T>();

syntax’ını kullanarak, framework’e T tipindeki interface’lerimiz için mock işlemini gerçekleştireceğimizi belirttik. Daha sonra “ChangeStock_WhenProductNotNull_Change” isminde bir örnek senaryo oluşturduk. Bu senaryo gereği stok değiştirme işleminin, product objesinin null gelmediği durumlarda değişmesini beklemekteyiz. Bunun için method’un “Arrange” kısmında, ProductStockService class’ının “ChangeStock” işlevini yerine getirebilmesi için bağımlılığı bulunduğu “ProductRepository” ve “StockRepository” için senaryonun başarılı olup, business logic’i test edebilmemiz adına, istediğimiz doğrultuda mock objeler oluşturduk. Bunun için:

Arg.Is<T>(expression)

syntax’ı ile, T tipinin int olacağını ve 10’dan küçük olacağını belirttik. Bu işlemin sonucunda ise, test senaryomuzun başarılı çalışabilmesi adına, geriye fake bir Product objesi dönmesi gerektiğini söyledik. İkinci satırında ise, bu işlemler sonucunda “StockRepository” içerisindeki “ChangeStock” method’ununda:

Arg.Any<Product>(), Arg.Any<int>()

parametreleri ile Product ve int tipinde herhangi bir objenin gelebileceği belirttik. Bunun sonucunda ise senaryomuz gereği başarılı bir sonuç beklediğimiz için “true” değerini dönmesini gerektiğini söyledik. Act kısmına baktığımızda ise artık ProductStockService class’ı üzerinde “ChangeStock” method’unu, productId’si “5” ve stock değeri ise “50” olarak çağırdık.

Bu işlemler sonucunda mock objelerimiz devreye gireceği için ilgili servis sınıfı içerisindeki business logic başarılı çalışacaktır ve Assert kısmı true değerini alarak test senaryomuz başarıyla gerçekleşmiş olacaktır.

Bir diğer senaryomuz olan “ChangeStock_WhenProductNull_WriteLogMessage” method’una baktığımızda ise, Product objesinin null olduğu durumlarda, başarılı bir şekilde log atıp atamatığımıza bakmaktayız. Hiç bir mock objesi kullanmadığımız için ilgili servis çağrısı yapıldığında Product objesi null olacağı için “_logger.Log()” method’u devreye girecektir. Assert kısmında ise yine NSubstitute Framework’ünün bize sağlamış olduğu “Received” extension’u ile, “_logger” class’ı içerisindeki “Log” method’unun herhangi bir string parametresi ile çağrılıp çağrılmadığını kontrol ettirmekteyiz.

Bunun gibi bir çok kolaylık sunan bu extension’lara bakmak isterseniz eğer, buradan ulaşabilirsiniz.

Böylelikle bir makalemin daha sonuna geldik. Bir sonraki makalelerimde görüşmek dileğiyle.  Örnek olarak gerçekleştirmiş olduğumuz projeye ekten ulaşabilirsiniz.

UnitTestMockingSample

Gökhan Gökalp

View Comments

  • Güzel bir yazı, teşekkürler. Kod içermeyen kaynaklardan bazılarını kullanmamın bir sakıncası yoktur umarım? Jmockit tanıtım yazısı ile mock-up tanıtımı yapacağım ve güzel derlediğinizi düşünerek kullanmayı düşündüm. Elinize sağlık

  • Makale için teşekkürler. Code coverage ile ilgili yazı yazmayı düşünüyor musunuz? Türkçe kaynak cok yok bu konuda. O da baya faydalı olabilirdi öğrenmek isteyenler için.

    Elinize sağlık.

Recent Posts

Event-Driven Architecture’larda Conditional Claim-Check Pattern’ı ile Event Boyut Sınırlarının Üstesinden Gelmek

{:en}In today’s technological age, we typically build our application solutions on event-driven architecture in order…

2 ay ago

Containerized Uygulamaların Supply Chain’ini Güvence Altına Alarak Güvenlik Risklerini Azaltma (Güvenlik Taraması, SBOM’lar, Artifact’lerin İmzalanması ve Doğrulanması) – Bölüm 1

{:tr}Bildiğimiz gibi modern yazılım geliştirme ortamında containerization'ın benimsenmesi, uygulamaların oluşturulma ve dağıtılma şekillerini oldukça değiştirdi.…

9 ay ago

Identity & Access Management İşlemlerini Azure AD B2C ile .NET Ortamında Gerçekleştirmek

{:tr}Bildiğimiz gibi bir ürün geliştirirken olabildiğince farklı cloud çözümlerinden faydalanmak, harcanacak zaman ve karmaşıklığın yanı…

1 yıl ago

Azure Service Bus Kullanarak Microservice’lerde Event’ler Nasıl Sıralanır (FIFO Consumers)

{:tr}Bazen bazı senaryolar vardır karmaşıklığını veya eksi yanlarını bildiğimiz halde implemente etmekten kaçamadığımız veya implemente…

2 yıl ago

.NET Microservice’lerinde Outbox Pattern’ı ile Eventual Consistency için Atomicity Sağlama

{:tr}Bildiğimiz gibi microservice architecture'ına adapte olmanın bir çok artı noktası olduğu gibi, maalesef getirdiği bazı…

2 yıl ago