Caner Tosuner

Leave your code better than you found it

Moq Library Kullanarak Unit Test Yazma

Daha önceki yazımızda Unit Test Nedir Nasıl Yazılır konusuna değinmiştik ve basit bir console uygulaması ve onun unit test metotlarının bulunduğu test projemizi yazmıştık. O örneğimizde herhangi bir database veya kendi oluşturduğumuz data modelleri vs yoktu 4 işlem yapan bir projeydi. Bu yazımızda Db operasyonları olan bir projede unit test yazmak istesek ne yapardık bu soruya cevap arıyor olacağız.

Bir UserRepository class'ımız olsun ve bu repository için unit test metotları yazıyor olalım. Peki ama test metotlarını yazarken nasıl bir yol izleyeceğiz ? Her bir test case'i için gidip database saçma sapan fake kayıtlar atıp CRUD işlemleri yapmamamız gerekir. Bu gibi durumlar için mocking dediğimiz "alaycı" veya "sahte" kayıtlar oluşturmamızı sağlayan library'ler bulunmakta. Bu library'lerden Moq'u kullanarak sahte nesneler üreterek UserRepository için basit bir test projesi yazacağız.

Moq

Moq .Net tarafında unit test yazmada kullanabildiğimiz bir mocking kütüphanesidir. Testlerimiz için sahte nesneler üreterek normal projemizde ki case'leri test etmemizi sağlar.

Örnek projemiz için öncelikle bir tane UserSample adımda Console Application oluşturalım ve içerisine User.cs ve IUserRepository.cs class'larını aşağıdaki gibi tanımlayalım.

    public class User
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
    public interface IUserRepository
    {
        IList<User> GetAll();
        User GetById(int userId);
        void Insert(User user);
        void Update(User user);
        void Delete(int Id);
    }

Şimdi ise UserSample.Test adında test projemizi oluşturalım ve UserSample projemizi referans olarak test projemize ekleyelim. Sonrasında test projemize Nuget üzerinden Moq kütüphanesini kuralım.

Tools > Nuget Package Manager > Package Manager Console > PM> Install-Package Moq

 Kurulum işlemi tamamlandıktan sonra UserRepositoryTest adında bir class oluşturalım.

İlk olarak User Repository için gerekli olan setup işlemlerini yapalım. Setup işlemi kısaca repository'nin içerisindeki metotların sahte objelerle işlemleri yapmasını sağlayacak kodları yazmak diyebiliriz. Mocking setup ile ilgili kodlarımız aşağıdaki gibi olacaktır.

 [TestClass]
    public class UserRepositoryTest
    {
        public readonly IUserRepository MockUserRepository;

        public UserRepositoryTest()
        {
            // Test metotları genelinde kullanacağımız User listesi
            var userList = new List<User>
            {
                new User {Id=1,FirstName="User1",LastName="User1LastName" },
                new User {Id=2,FirstName="User2",LastName="User2LastName" },
                new User {Id=3,FirstName="User3",LastName="User3LastName" }
            };

            // Mock the Products Repository using Moq
            var mockUserRepository = new Mock<IUserRepository>();

            // GetAll metodu için setup işlemi
            mockUserRepository.Setup(mr => mr.GetAll()).Returns(userList);

            // GetById metodu için setup işlemi
            mockUserRepository.Setup(mr => mr.GetById(It.IsAny<int>())).Returns((int i) => userList.Single(x => x.Id == i));

            // Insert için setup işlemi
            mockUserRepository.Setup(mr => mr.Insert(It.IsAny<User>())).Callback(
                (User target) =>
                {
                    userList.Add(target);
                });

            // Update için setup işlemi
            mockUserRepository.Setup(mr => mr.Update(It.IsAny<User>())).Callback(
                (User target) =>
                {
                    var original = userList.Where(q => q.Id == target.Id).Single();

                    if (original == null)
                    {
                        throw new InvalidOperationException();
                    }

                    original.FirstName = target.FirstName;
                    original.LastName = target.LastName;

                });

            // Test metotlarından erişilebilmesi için global olarak tanımladığımız MockUserRepository'e yukarıdaki setup işlemlerini atıyoruz
            this.MockUserRepository = mockUserRepository.Object;
        }
}

Yukarıda bulunan kodlar kısaca şunları söylemekte;

Arkadaş senin IUserRepository diye CRUD işlemlerinin yapıldığı bir class'ın var ve bu class içerisinde bulunan GetAll, GetById, Insert, Update, Delete metotları için tanımlanan mocking veya kandırmaca işlemleri yukarıdaki gibidir. Sen Database üzerinden bu işlemleri yapmak yerine rahatça userList array'i üzerinden bu işlemleri yapabilirsin.

Buraya kadar her şey OK ise aşağıdaki gibi sırasıyla test metotlarımızı yazalım.

GetAll metodunu çağırarak bize veri döndüğünü gösteren test metodu.

        [TestMethod]
        public void GetAll_Than_Check_Count_Test()
        {
            var expected = this.MockUserRepository.GetAll().Count;

            Assert.IsNotNull(expected);// Test not null
            Assert.IsTrue(expected > 0);// Test GetAll returns user objects
        }

GetById metodu için doğru objeyi return edip etmediği durumu için test metodu.

        [TestMethod]
        public void GetById_Than_Check_Correct_Object_Test()
        {
            var actual = new User { Id = 2, FirstName = "User2", LastName = "User2LastName" };

            var expected = this.MockUserRepository.GetById(2);

            Assert.IsNotNull(expected); // Test is not null
            Assert.IsInstanceOfType(expected, typeof(User)); // Test type
            Assert.AreEqual(actual.Id, expected.Id); // test correct object found
        }

Insert işleminden sonra GetAll metodundan dönen object sayısı doğrumu testi

        [TestMethod]
        public void Insert_User_Than_Check_GetAll_Count_Test()
        {
            var actual = this.MockUserRepository.GetAll().Count + 1;

            var user = new User { Id = 4, FirstName = "User4", LastName = "User4LastName" };

            this.MockUserRepository.Insert(user);

            var expected = this.MockUserRepository.GetAll().Count;

            Assert.AreEqual(actual, expected);
        }

GetById metoduna hatalı bir Id ile çağrım yapıldığında Exception döneceği durumu için test metodu.

        [TestMethod]
        [ExpectedException(typeof(InvalidOperationException))]//Eğer beklediğimiz bir exception var ise bu şekilde tanımlayabiliriz
        public void GetyId_With_Undefined_Id_Than_Exception_Occurred_Test()
        {
            var expected = this.MockUserRepository.GetById(It.IsAny<int>());
        }

Update işlemi sonrasında GetById yapılarak dönen nesnede bulunan değerler doğrumu test metodu.

        [TestMethod]
        public void Ipdate_User_Than_Check_It_Is_Updated_Test()
        {
            var actual = new User { Id = 2, FirstName = "User2_Updated", LastName = "User2LastName_Updated" };

            this.MockUserRepository.Update(actual);

            var expected = this.MockUserRepository.GetById(actual.Id);

            Assert.IsNotNull(expected);
            Assert.AreEqual(actual.FirstName, expected.FirstName);
            Assert.AreEqual(actual.LastName, expected.LastName);
        }

Test metotlarını yazdıktan sonra Run All Tests diyerek testlerimizi çalıştırıp success fail durumlarını görebiliriz.

Bu yazımızda bir Mocking kütüphanesi olan Moq kullanarak basitçe bir Unit Test projesi hazırladık ve halen daha çok fazla önemsenmese de unit Test dev-ops süreçlerinin olgunlaşmasıyla artık bir çok firma için "IsMust" zorunlu hale gelmiş bir kuraldır ve daha önceki yazılarda da bahsettiğim üzre Unit Test yazıyor olmak artık interview'larda beklenen bir durum haline gelmiştir.

Unit Test Nedir

Unit Test günümüz yazılım dünyasında artık olmazsa olmaz bir kural olarak bütün projelerde yazılması zorunlu hale gelmekte ve mülakatlarda Unit Test yazabiliyorum diyebilmek bile biz developer lar için zamanla ciddi bir bonus oluyor. DevOps açısından düşündüğümüzde Continuous Integration ve Continuous Deployment tool'ları yazmış olduğumuz kodları build edip deploy etmeden önce ilk olarak test metotlarını çalıştırır ve metotlardan herhangi biri fail verdiğinde deploy'u durdurup developer'ı bilgilendirir. Bu akış . Kısaca Unit Test artık Nice to Have olmaktan çıkıp Is Must hale gelmektedir. Bu yazımızda Unit Test nedir, nasıl yazılır gibi sorulara cevap arıyor olacağız.

Unit Test Nedir ?

Unit test geliştirmiş olduğunuz kodu test etmek için yazdığınız kod dur ve esas olarak kodun veya fonksiyonun belirli bir biriminin davranışını kontrol eder ve geliştirmeyi yapan developer tarafından yazılır.

Unit test aynı zamanda kodlarınızı kullanacak diğer developer'lar tarafından okunduğunda ise onların kodları nasıl kullanması gerektiğini anlatması açısından da önemli bir göreve sahiptir. Nasıl çalıştığını öğrenmek için testleri çalıştırıp okuyabilirler.

Bazı Unit Test Frameworkleri

  1. MSTest
  2. NUnit
  3. xUnit
  4. MBunit

 

Örnek Proje

Şimdi küçük bir unit test örneği yapalım. Eft işlemi yapan bir modül için bir Console Application ve bu modülü basitçe test etmek için bir unit test projesi oluşturalım.

İlk olarak VS da EftSample adında bir console projesi oluşturalım ve sonrasında solution'a sağ tıklayıp yeni bir proje ekle diyerek ismi EftSample.Test adında bir UnitTest projesi oluşturalım.

Eft işleminde kullanılacak olan TransferRequest ve TransferResponse adında class'larını console uygulamamıza aşağıdaki gibi oluşturalım.

       public class TransferRequest
       {
           public decimal Amount { get; set; }
           public string SenderIBAN { get; set; }
           public string ReceiverIBAN { get; set; }
       }
 
       public class TransferResponse
       {
           public bool IsSuccess { get; set; }
           public string Message { get; set; }
       }

Daha sonra transfer işlemini yapacak olan metodu Transfer adında bir class içerisine TransferMoney adında bir metot aşağıdaki gibi tanımlayalım.

Transfer.cs

    public class Transfer
    {
        public TransferResponse MoneyTransfer(TransferRequest request)
        {
            var response = new TransferResponse();
            
            //Aşağıdaki validasyonlar için farklı yöntemler, frameworkler kullanılması daha doğrudur ancak açık görünmesi açısından bu şekilde yapalım
            if (request.Amount < 1)
            {
                response.IsSuccess = false;
                response.Message = "Amount must be greater Then 1";
                return response;
            }
            if (string.IsNullOrEmpty(request.SenderIBAN))
            {
                response.IsSuccess = false;
                response.Message = "Sender Account IBAN must not be null or empty.";
                return response;
            }
            if (string.IsNullOrEmpty(request.ReceiverIBAN))
            {
                response.IsSuccess = false;
                response.Message = "Receiver Account IBAN must not be null or empty.";
                return response;
            }

            //TODO do process
            response.IsSuccess = true;
            response.Message = "Transfer operation is successful.";

            return response;
        }
    }

Şimdi sıra test metotlarını yazmaya geldi. EftSample.Test adında ki test projemize gidip EftSample projemizi referans olarak ekleyelim. Sonrasında TransferMoneyTest.cs adında test metotlarımızı yazacağımız class'ı oluşturalım.

Not - 1: Unit test yazarken naming oldukça önemli bir konudur. Ortak kabul görmüş bir standart olan MethodName_StateUnderTest_ExpectedBehavior şeklinde bir isimlendirme önerilir ancak her yiğidin yoğurt yiyişi farklıdır tezinden yola çıkarak developer anlaşılır yazdığı sürece isimlendirmeyi istediği gibide yapabilmektedir. 

Not - 2: Test metotlarını yazdığımız class'ların başında [TestClass] adında bir attribute tanımlaması yapılması zorunludur. Bu attribute ile sistem o class'ın bir test class'ı olduğunu anlamakta.

Not - 3: Yazılmış olan test metotlarının başında [TestMethod] attribute'ü yazılması gerekir. Bu attribute o metodun bir test metodu olduğunu işaret etmekte.

1- Amount Less Then One Test

İlk test metodumuz Amount = 0 olarak eft işlemi yapmaya çalıştığımızda ki case'i test ediyor olsun.

        [TestMethod]
        public void TransferMoney_With_Zero_Amount_Then_Transfer_Failed_Test()
        {
            var expected = new TransferResponse
            {
                IsSuccess = false,
                Message = "Amount must be greater Then 1"
            };

            var transfer = new Transfer();

            var transferRequest = new TransferRequest
            {
                Amount = 0,
                ReceiverIBAN = "TR806541651616516416541",
                SenderIBAN = "TR13216498468465416"
            };

            var actual = transfer.MoneyTransfer(transferRequest);

            Assert.AreEqual(expected.IsSuccess, actual.IsSuccess);
            Assert.AreEqual(expected.Message, actual.Message);
        }

2- Sender IBAN Empty Test

İkinci test metodu ise sender iban alanı boş bırakıldığında ki case'i test ediyor olsun.

        [TestMethod]
        public void TransferMoney_With_Empty_Sender_IBAN_Then_Transfer_Failed_Test()
        {
            var expected = new TransferResponse
            {
                IsSuccess = false,
                Message = "Sender Account IBAN must not be null or empty."
            };

            var transfer = new Transfer();

            var transferRequest = new TransferRequest
            {
                Amount = 60,
                SenderIBAN = string.Empty,
                ReceiverIBAN = "TR13216498468465416"
            };

            var actual = transfer.MoneyTransfer(transferRequest);

            Assert.AreEqual(expected.IsSuccess, actual.IsSuccess);
            Assert.AreEqual(expected.Message, actual.Message);
        }

3- All Pramters Are Ok Test

Son metodumuz da doğru bilgilerle transfer işlemi yapılmaya çalışıldığında ki case'i test ediyor olsun.

        [TestMethod]
        public void TransferMoney_With_Correct_Request_Parameter_Then_Transfer_OK_Test()
        {
            var expected = new TransferResponse
            {
                IsSuccess = true,
                Message = "Transfer operation is successful."
            };

            var transfer = new Transfer();

            var transferRequest = new TransferRequest
            {
                Amount = 60,
                ReceiverIBAN = "TR806541651616516416541",
                SenderIBAN = "TR13216498468465416"
            };

            var actual = transfer.MoneyTransfer(transferRequest);

            Assert.AreEqual(expected.IsSuccess, actual.IsSuccess);
            Assert.AreEqual(expected.Message, actual.Message);
        }

Test metotlarımızı yazdıktan sonra aşağıdaki görselde olduğu gibi VS'da bulunan Test > Run > All Tests dedikten sonra Test Explorer açılır ve solution da bulunan test metotları çalıştırılıp success ve fail durumlarını görebiliriz.

TransferMoneyTest.cs içerisi son olarak aşağıdaki gibidir.

    [TestClass]
    public class TransferMoneyTest
    {
        [TestMethod]
        public void TransferMoney_With_Zero_Amount_Then_Transfer_Failed_Test()
        {
            var expected = new TransferResponse
            {
                IsSuccess = false,
                Message = "Amount must be greater Then 1"
            };

            var transfer = new Transfer();

            var transferRequest = new TransferRequest
            {
                Amount = 0,
                ReceiverIBAN = "TR806541651616516416541",
                SenderIBAN = "TR13216498468465416"
            };

            var actual = transfer.MoneyTransfer(transferRequest);

            Assert.AreEqual(expected.IsSuccess, actual.IsSuccess);
            Assert.AreEqual(expected.Message, actual.Message);
        }

        [TestMethod]
        public void TransferMoney_With_Empty_Sender_IBAN_Then_Transfer_Failed_Test()
        {
            var expected = new TransferResponse
            {
                IsSuccess = false,
                Message = "Sender Account IBAN must not be null or empty."
            };

            var transfer = new Transfer();

            var transferRequest = new TransferRequest
            {
                Amount = 60,
                SenderIBAN = string.Empty,
                ReceiverIBAN = "TR13216498468465416"
            };

            var actual = transfer.MoneyTransfer(transferRequest);

            Assert.AreEqual(expected.IsSuccess, actual.IsSuccess);
            Assert.AreEqual(expected.Message, actual.Message);
        }

        [TestMethod]
        public void TransferMoney_With_Correct_Request_Parameter_Then_Transfer_OK_Test()
        {
            var expected = new TransferResponse
            {
                IsSuccess = true,
                Message = "Transfer operation is successful."
            };

            var transfer = new Transfer();

            var transferRequest = new TransferRequest
            {
                Amount = 60,
                ReceiverIBAN = "TR806541651616516416541",
                SenderIBAN = "TR13216498468465416"
            };

            var actual = transfer.MoneyTransfer(transferRequest);

            Assert.AreEqual(expected.IsSuccess, actual.IsSuccess);
            Assert.AreEqual(expected.Message, actual.Message);
        }
    }

Çok farklı frameworkler kullanarak çok farklı test yapıları oluşturabilirsiniz. Yaptığımız örnekte basitçe unit test nasıl yazılır göstermeye çalıştık. Unit test ile ilgili daha fazla bilgiyi burada bulunan linkten bulabilirsiniz.