Caner Tosuner

Leave your code better than you found it

Repository Pattern CRUD İşlemleri Dışında Bulunan Specific Metotlar İçin Mocking

Bir önceki yazımızda Repository Pattern için Mocking Infrastructure Oluşturma konusuna değinmiştik ve CRUD işlemleri için ortak bir setup yapısı oluşturmuştuk. Peki ya aşağıdaki sorulduğu gibi bir case ile karşılaşırsak;

Soru : Crud metotları dışında sadece o repository'e özel bir metot tanımlamak istersek setup işlemi için nasıl bir yol izlemeliyiz ? 

Örneğin UserRepository için bir önceki örnekte tanımladığımız tanımladığımız All, Get, Insert, Update, Delete metotlarının dışında bir de GetByEmail() adında bir metot gerekli. Bu metot için gidip IRepository içerisine yeni bir metot eklemek ve sonrasında RepositoryBaseTest içerisine setup tanımlaması yapmak doğru olmaz çünkü orası adından da anlaşıldığı üzre Base anlayışına uyan işlemler için sınırlandırılmış bir yer. 

Bu gibi durumlarda IUseRepository adında bir interface tanımlayıp ve UserRepository'yi aşağıdaki gibi modify etmemiz yeterli olacaktır.

    public interface IUserRepository
    {
        User GetByEmail(string email);
    }

public class UserRepository : BaseRepository<int, User>, IUserRepository
    {
        public User GetByEmail(string email)
        {
            throw new NotImplementedException();
        }
    }

Test tarafındaki mocking işlemi için ise IUserRepository interface'ini mock yaparak setup işlemini tamamlayabiliriz.

UserRepositoryTest class'ının son hali aşağıdaki gibidir.

    [TestClass]
    public class UserRepositoryTest : RepositoryBaseTest
    {
        private List<User> _userList;
        private Mock<IRepository<int, User>> _mockRepo;
        private Mock<IUserRepository> _mockUserRepo;

        [TestInitialize]
        public void Setup()
        {
            _userList = new List<User>();
            var user1 = new User
            {
                Id = 1,
                Email = "canertosuner@gmail.com",
                FirstName = "Caner",
                LastName = "Tosuner"
            };
            _userList.Add(user1);

            var user2 = new User
            {
                Id = 2,
                Email = "tanertosuner@gmail.com",
                FirstName = "Taner",
                LastName = "Tosuner"
            };
            _userList.Add(user2);

            var user3 = new User
            {
                Id = 3,
                Email = "janertosuner@gmail.com",
                FirstName = "Janer",
                LastName = "Tosuner"
            };
            _userList.Add(user3);

            var user4 = new User
            {
                Id = 4,
                Email = "yenertosuner@gmail.com",
                FirstName = "Yeneer",
                LastName = "Tosuner"
            };
            _userList.Add(user4);

            _mockRepo = new Mock<IRepository<int, User>>();

            // mock common methods
            SetupRepositoryMock<int, User>(_mockRepo, _userList);

            _mockUserRepo = new Mock<IUserRepository>();

            // mock specific method
            _mockUserRepo.Setup(x => x.GetByEmail(It.IsAny<string>()))
                .Returns(new Func<string, User>(
                    email => _userList.Single(x => x.Email == email))
                );
        }

        [TestMethod]
        public void Get_By_Email_Then_Result_OK()
        {
            var userFirst = _mockRepo.Object.All().FirstOrDefault();

            var userByEmail = _mockUserRepo.Object.GetByEmail(userFirst.Email);

            Assert.IsNotNull(userByEmail);
            Assert.AreEqual(userFirst.Email, userByEmail.Email);
            Assert.AreEqual(userFirst.Id, userByEmail.Id);
        }
    }

Yukarıda olduğu gibi ihtiyacımız olan metodu interface aracılığıyla soyutlaştırarak common olan ortak metotlar dışında ayrı olarak mocking işlemi yapabiliriz.

Repository Katmanı için Mocking Infrastructure Oluşturma (Moq Library)

Daha önceki Unit Test yazılarımızda Unit Test Nedir Nasıl Yazılır ve Moq Library Kullanarak Unit Test Yazma konularına değinmiştik. Bu yazımızda ise çokça kullandığımız Repository Pattern CRUD işlemlerinin yapıldığı metotlar için reusable bir mocking yapısı oluşturacağız. 

Öncelikle VS'da RepositoryMocking adında bir proje oluşturalım ve sonrasında projemize Generic Repository ile ilgili tanımlamalarımızı yapalım. İlk olarak IRepository adında bir interface ve database de bulunan tablolardaki unique Id-primary key alanına karşılık gelen generic IUniqueIdentifier interface'ini oluşturalaım. Bu interface'i oluşturmamızdaki amaç her tabloda Id alanı farklı tiplerde olabilir bu nedenle objelerimizi oluştururken IUniqueIdentifier interface'inden implement ederek Id alanı için veri tipini belirteceğiz. Bu bize test metotlarımızı tanımlarken ilgili linq sorgularını oluşturmada yarar sağlayacak.

public interface IUniqueIdentifier<Tkey>
{
    TKey Id { get; set; }
}
 
public interface IRepository<Tkey,TEntity> where TEntity : IUniqueIdentifier<Tkey>
{
    IQueryable<TEntity> All();
    TEntity Get(TKey Id);
    TEntity Add(TEntity entity);
    void Update(TEntity entity);
    void Delete(TEntity entity);
}

Şimdi ise abstract olan ve IRepository den inherit olan BaseRepository class'ını oluşturalım.

    public abstract class BaseRepository<TKey, TEntity> : IRepository<TKey, TEntity> where TEntity : IUniqueIdentifier<TKey>
    {
        public IQueryable<TEntity> All()
        {
            throw new NotImplementedException();
        }

        public TEntity Get(TKey id)
        {
            throw new NotImplementedException();
        }

        public TEntity Add(TEntity entity)
        {
            throw new NotImplementedException();
        }

        public void Update(TEntity entity)
        {
            throw new NotImplementedException();
        }

        public void Delete(TEntity entity)
        {
            throw new NotImplementedException();
        }
    }

Mocking işlemi yapacağımızdan metot içlerini doldurmadım ancak tabikide ilgili linq sorgularının yazılmasını gerekir.

BaseRepository tanımlamasını da yaptıktan sonra database de bulunan User tablosu için bir object ve bu tabloya ait UserRepository class'ını oluşturalım. 

    public class User : IUniqueIdentifier<int>
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
    }
    public class UserRepository : BaseRepository<int, User>
    {

    }

Buraya kadar olan kısımda Repository katmanı için gerekli olan her şey hazır. Artık Test projemizi oluşturabiliriz. Solution'a RepositoryMocking.UnitTest adında yeni bir test projesi oluşturalım ve içerisine RepositoryBaseTest adında bir class ekleyelim. Bu class reusable mocking setup işlemlerini yapacağımız class olacak.

 public abstract class RepositoryBaseTest
    {
        public void SetupRepositoryMock<TK, TE>(Mock mockRepo, List<TE> data) where TE : class, IUniqueIdentifier<TK>
        {
            var mock = mockRepo.As<IRepository<TK, TE>>();

            // setup All method
            mock.Setup(x => x.All()).Returns(data.AsQueryable());

            // setup Add method
            mock.Setup(x => x.Add(It.IsAny<TE>()))
                .Returns(new Func<TE, TE>(x =>
                {
                    dynamic lastId = data.Last().Id;
                    dynamic nextId = lastId + 1;
                    x.Id = nextId;
                    data.Add(x);
                    return data.Last();
                }));

            // setup Update method
            mock.Setup(x => x.Update(It.IsAny<TE>()))
                .Callback(new Action<TE>(x =>
                {
                    var i = data.FindIndex(q => q.Id.Equals(x.Id));
                    data[i] = x;
                }));

            // setup Get method
            mock.Setup(x => x.Get(It.IsAny<TK>()))
                .Returns(new Func<TK, TE>(
                    x => data.Find(q => q.Id.Equals(x))
                ));

            // setup Delete
            mock.Setup(x => x.Delete(It.IsAny<TE>()))
                .Callback(new Action<TE>(x =>
                {
                    var i = data.FindIndex(q => q.Id.Equals(x.Id));
                    data.RemoveAt(i);
                }));
        }
    }

Üstte bulunan kodlarda BaseRepository de bulunan db için All, Get, Insert, Update ve Delete işlemlerini yapacak olan metotlar için ortak bir setup yapısı oluşturduk ve UserRepository gibi diğer oluşturacağınız repository ler içinde RepositoryBaseTest class'ını kullanabileceğiz. Buda bizi her bir repository için ayrı ayrı setup işlemleri yapmaktan kurtarıyor. IRepository interface'ine yeni bir metot eklemek istediğinizde tekrardan yukarıda yazdığımız SetupRepositoryMock içerisine bu metot için gerekli setup işlemini tanımlayabiliriz. 

Şimdi ise UserRepository için UserRepositoryTest adında bir sınıf oluşturalım ve RepositoryBaseTest class'ını kullanarak mock işlemleri yapalım.

    [TestClass]
    public class UserRepositoryTest: RepositoryBaseTest
    {
        //db de bulunan tablo yerine geçecek fake tablomuz
        private List<User> _userList;

        //mock user repository
        private Mock<IRepository<int, User>> _mockRepo;

        [TestInitialize]
        public void Setup()
        {
            //tablomuzun içerisini dolduralım
            _userList = new List<User>();
            var user1 = new User
            {
                Id = 1,
                Email = "canertosuner@gmail.com",
                FirstName = "Caner",
                LastName = "Tosuner"
            };
            _userList.Add(user1);

            var user2 = new User
            {
                Id = 2,
                Email = "tanertosuner@gmail.com",
                FirstName = "Taner",
                LastName = "Tosuner"
            };
            _userList.Add(user2);

            var user3 = new User
            {
                Id = 3,
                Email = "janertosuner@gmail.com",
                FirstName = "Janer",
                LastName = "Tosuner"
            };
            _userList.Add(user3);

            var user4 = new User
            {
                Id = 4,
                Email = "yenertosuner@gmail.com",
                FirstName = "Yeneer",
                LastName = "Tosuner"
            };
            _userList.Add(user4);
             
            //mock respository değerini initialize edelim
            _mockRepo = new Mock<IRepository<int, User>>();

            //repositorybasetest class'ını kullarak crud metotlarını için setup işlemlerini yapalım
            SetupRepositoryMock<int, User>(_mockRepo, _userList);
        }
    }

UserRepository için setup işlemlerimizi tamamladık. Yukarıdaki işlemler sonrasında elimizde db de bulunan User tablosu yerine geçen bir _userList array'imiz ve bu array üzerinden repository metotlarını setup ettik. Şimdi bir kaç test metodu yazıp kodlarımızı test edelim. UserRepositoryTest class'ının son hali aşağıdaki gibidir.

    [TestClass]
    public class UserRepositoryTest: RepositoryBaseTest
    {
        private List<User> _userList;
        private Mock<IRepository<int, User>> _mockRepo;

        [TestInitialize]
        public void Setup()
        {
            _userList = new List<User>();
            var user1 = new User
            {
                Id = 1,
                Email = "canertosuner@gmail.com",
                FirstName = "Caner",
                LastName = "Tosuner"
            };
            _userList.Add(user1);

            var user2 = new User
            {
                Id = 2,
                Email = "tanertosuner@gmail.com",
                FirstName = "Taner",
                LastName = "Tosuner"
            };
            _userList.Add(user2);

            var user3 = new User
            {
                Id = 3,
                Email = "janertosuner@gmail.com",
                FirstName = "Janer",
                LastName = "Tosuner"
            };
            _userList.Add(user3);

            var user4 = new User
            {
                Id = 4,
                Email = "yenertosuner@gmail.com",
                FirstName = "Yeneer",
                LastName = "Tosuner"
            };
            _userList.Add(user4);

            _mockRepo = new Mock<IRepository<int, User>>();

            SetupRepositoryMock<int, User>(_mockRepo, _userList);
        }

        [TestMethod]
        public void Get_All_Count()
        {
            Assert.AreEqual(_userList.Count, _mockRepo.Object.All().Count());
        }

        [TestMethod]
        public void Get_By_Id_Then_Check_Name()
        {
            var item = _mockRepo.Object.FindBy(4);
            Assert.AreEqual("Yeneer", item.FirstName);
        }

        [TestMethod]
        public void Remove_User_Then_Check_Count()
        {
            var user4 = new User
            {
                Id = 4,
                Email = "yenertosuner@gmail.com",
                FirstName = "Yeneer",
                LastName = "Tosuner"
            };
            _mockRepo.Object.Delete(user4);
            Assert.AreEqual(3, _mockRepo.Object.All().Count());
        }

        [TestMethod]
        public void Add_New_User_Then_Check_Count()
        {
            var tempCount = _userList.Count;

            var user5 = new User
            {
                Email = "yenertosuner@gmail.com",
                FirstName = "Yeneer",
                LastName = "Tosuner"
            };
            _mockRepo.Object.Add(user5);

            Assert.AreEqual(tempCount + 1, _mockRepo.Object.All().Count());
        }
    }

RepositoryPattern için reusable mocking işlemi için hepsi bu kadar. Projenizde UserRepository dışında bulunan diğer repository'ler içinde aynı UserRepository de olduğu gibi generic oluşturduğumuz RepositoryBaseTest'i kullanarak setup işlemini yapıp testlerinizi yazabilirsiniz.

Repository Design Pattern

Çoğu yazılım uygulamasının belli mimari kurallar çerçevisinde farklı katmanlara ayrılarak geliştirilmesi beklenir ve buna n-tier architecture denir. N-tier architecture presentation tier, middle tier yada data tier gibi çeşitli katmanlardan oluşabilir. Presentation katmanında uygulamanızın UI tarafını ilgilendiren arayüzler bulunur, Middle katmanda business logic'i ilgilendiren geliştirmeler yapılır ve aynı zamanda database ile etkileşimde bu katmanda bulunur. Data katmanı ise uygulamanızdaki SQL Server, Oracle gibi database context'lerini içeren katmandır. Bu katman belkide uygulamadaki en önemli katmandır sebebi ise database. Data bizim için en önemli şey ve database'ler ile olan veri alış verişi işin en güvenlikli ve generic olması gereken yerlerin başında gelir.

Database varsa CRUD işlemi (Create, Read, Update, Delete) heralde olmazsa olmazdır ve data katmanı ile doğrudan business logic'in bulunduğu middle katman haberleşsin istenilmez ve bu durum bazen uygulamanızın büyüklüğüne göre çeşitli sorunlarda çıkartabilir. Repository pattern bu ihtiyacı karşılayan pattern olarak karşımıza çıkıyor ve temelde yaptığı iş ise business logic ile data access arasında arabulucu görevi görme.

Repository Pattern Faydaları

  • Maintainability (sonradan bakım kolaylılığı) arttırır,
  • Unit-test yapabilmemizi kolaylaştırır,
  • Esnek bir mimari uygulamamızı sağlar,
  • Yeni gelecek modüller veya istenilen değişiklikleri kolayca entegre edebilmeyi sağlar,
  • Domain driven development'ın önünü açar.

 

Repository Pattern Uygulaması

Şimdi örnek bir proje üzerinden Repository Pattern nasıl uygulanır görelim. Projeyi geliştirirken projenizde EntityFramework kurulu ve SampleDbContext adında bir database context'inizin olduğunu varsayarak ilerliyor olacağız.

1.Adım - IRepository adında CRUD işlemleri yapabilmemizi sağlayacak Generic bir interface metodlarını tanımlama.

public interface IRepository<T>  where T : class
    {
        IEnumerable<t> SelectAll();
        T SelectByID(object id);
        void Insert(T obj);
        void Update(T obj);
        void Delete(object id);
        void Save();
    }

2.Adım - BaseRepository adında Generic IRepository interface'inden implement olan class'ı oluşturma.

public abstract class BaseRepository<T> : IRepository<T> where T : class
    {
        private SampleDbContext db = null;
        private DbSet<t> table = null;
        public BaseRepository()
        {
            this.db = new SampleDbContext();
            table = db.Set<T>();
        }
        public BaseRepository(SampleDbContext db)
        {
            this.db = db;
            table = db.Set<T>();
        }
        public IEnumerable<T> SelectAll()
        {
            return table.ToList();
        }
        public T SelectByID(object id)
        {
            return table.Find(id);
        }
        public void Insert(T obj)
        {
            table.Add(obj);
        }
        public void Update(T obj)
        {
            table.Attach(obj);
            db.Entry(obj).State = EntityState.Modified;
        }
        public void Delete(object id)
        {
            T existing = table.Find(id);
            table.Remove(existing);
        }
        public void Save()
        {
            db.SaveChanges();
        }
    }

3.Adım - Employee adında örnek bir Entity tanımlama

public class Employee
{
    public int ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

 

4.Adım - BaseRepository den inherit olan Employee entity si için EmployeeRepository class'ını tanımlama

  public class EmployeeRepository : BaseRepository<Employee>
    {

    }

5.Adım - Yazmış olduğumuz EmployeeRepository'yi kullanmak

  void Main(string[] args)
        {
            var empRepository = new EmployeeRepository();
            empRepository.SelectAll();
        }

Projemiz hazır. Bundan sonra tanımlayacağınız entity'ler için EmployeeRepository de olduğu gibi Generic olarak yazmış olduğumuz BaseRepository'den türeyen Repository'ler tanımlayarak projeyi geliştirmeye devam edebiliriz.

Not : Bir rivayete göre Repository Design Pattern, Design Pattern ailesi içerisinde en çok kullanılan pattern olduğu söylenir.