Caner Tosuner

Leave your code better than you found it

Asp.Net Core Unit Testing Database and Repository, In Memory Database Kullanımı

Asp.Net Core uygulamalarında unit test nedir nasl yazılır gibi konulara daha önceki yazımızda değinmiştik. O yazıdaki örnekte controller ve service layer'lar için nasıl unit test metotları yazabiliriz öğrenmiştik. Bu yazımızda ise bir diğer layer olan repository-layer için entity framework kullanılan bir projede nasıl unit testler yaratabiliriz inceleyeceğiz.

Entity framework core'un klasik entity framework'e kıyasla oldukça performanslı olmasıyla birlikte bazı artılarının olduğundan bahsetmiştik. Bu artılardan birisi de in-memory database option sunması (EF 6.1 ve sonrası içinde mevcut). Bu feature'dan önce repository'ler için unit test metotları yazmak istediğimizde Entity'lerin bulunduğu fake DbSet oluşturarak fake database ve tablolarını yaratmamız gerekiyordu. Yukarıda da belirttiğimiz üzre entity framework core ile birlikte in-memory database oluşturarak kolayca unit test sınıfları oluşturabiliriz.

Örnek projemiz üzerinden ilerleyecek olursak; bir tane asp.net core web application'ımız var ve sahip olduğu CustomerDbContext adında ki dbcontex'i kullanarak dışarıya end-point'ler açmakta. Hızlıca CustomerDbContext sınıfına bakacak olursak;

public class CustomerDbContext : DbContext
{
    public CustomerDbContext (DbContextOptions<CustomerDbContext > options)
        : base(options)
    {
    }

    public DbSet<Customer> Customer{ get; set; }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
    }
}

Startup.cs sınıfı içerisinde bulunan ConfigureServices metodunda ise CustomerDbContext'i constructor inejction uygulayarak base repository sınıfına taşıyacağımızdan context service olarak built-in container'a register edelim.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<CustomerDbContext >(options =>
        options.UseSqlServer(Configuration.GetSection("CustomerDbConnString").Value));

    services.AddScoped<ICustomerRepository, CustomerRepository>();

    services.AddMvc();
}

Yukarıdaki kod bloğunu basit bir şekilde anlatmak gerekirse,appsettings.json dosyasında yer alan connString adresini kullanarak CustomerDbContext'i bir sqlServer instance'ı ile ilişkilendirerek ayağa kaldırır. 

{
  "CustomerDbConnString": "Server=.;Initial Catalog=Customerdb;Persist Security Info=False;User ID=Customeruser;Password=qwerty135-;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=True;Connection Timeout=30;"
}

Amacımız CustomerRepository sınıfı için unit testler yazmak. GenericRepository pattern tercih etmiş olalım ve ilgili repository layer sınıflarımız aşağıdaki gibidir.

public interface IGenericRepository<T> where T : class, IEntity
{
    Guid Save(T entity);
    T Get(Guid id);
    void Update(T entity);
    void Delete(Guid id);
    IQueryable<T> All();
    IQueryable<T> Find(Expression<Func<T, bool>> predicate);
}
public abstract class GenericRepository<T> : IGenericRepository<T> where T : Entity
{
    private readonly CustomerDbContext _dbContext;
    private readonly DbSet<T> _dbSet;

    protected GenericRepository(CustomerDbContext dbContext)
    {
        this._dbContext = dbContext;
        this._dbSet = _dbContext.Set<T>();
    }

    public Guid Save(T entity)
    {
        entity.Id = Guid.NewGuid();
        _dbSet.Add(entity);
        _dbContext.SaveChanges();
        return entity.Id;
    }

    public T Get(Guid id)
    {
        return _dbSet.Find(id);
    }

    public void Update(T entity)
    {
        _dbSet.Attach(entity);
        _dbContext.Entry(entity).State = EntityState.Modified;
        _dbContext.SaveChanges();
    }

    public void Delete(Guid id)
    {
        var entity = Get(id);
        _dbSet.Remove(entity);
        _dbContext.SaveChanges();
    }

    public IQueryable<T> All()
    {
        return _dbSet.AsNoTracking();
    }

    public IQueryable<T> Find(Expression<Func<T, bool>> predicate)
    {
        return _dbSet.Where(predicate);
    }
}
public interface ICustomerRepository : IGenericRepository<Customer>
{ }
public class CustomerRepository : GenericRepository<Customer>, ICustomerRepository
{
    public CustomerRepository(CustomerDbContext dbContext) : base(dbContext)
    {
    }
}

CustomerDbContext'i constructor inejction uygulayarak base repository sınıfına taşıdık. Görüldüğü üzre CRUD işlemleri için metotları bulunan repository'nin unit testlerini yazacağız. Bunun için eski usul bir unit test db'si oluşturmak gibi çözümlere gitmeyeceğiz. Bunun yerine nuget üzerinden indirip kullanabileceğimiz Microsoft.EntityFrameworkCore.InMemory kütüphanesini kullanarak projemizde bir in-memory database ayağa kaldırabiliriz. İlgili kütüphaneyi nuget üzerinden projemiz referanslarına ekleyelim.

Kurulum tamamlandıktan sonra solution'da yeni bir xUnit test projesi oluşturalım ve ilk olarak repository'de ki Save metodu için aşağıdaki gibi unit-test metodunu yazalım.

[Fact]
public void Save_Should_Save_The_Customer_And_Should_Return_All_Count_As_Two()
{
    var customer1 = new Domain.Customer("Caner Tosuner", "IST", DateTime.Today.AddYears(28));
    var customer2 = new Domain.Customer("Caner Tosuner", "IST", DateTime.Today.AddYears(28));

    var options = new DbContextOptionsBuilder<CustomerDbContext>()
        .UseInMemoryDatabase("customer_db")
        .Options;

    using (var context = new CustomerDbContext(options))
    {
        var repository = new CustomerRepository(context);
        repository.Save(customer1);
        repository.Save(customer2);
        context.SaveChanges();
    }

    using (var context = new CustomerDbContext(options))
    {
        var repository = new CustomerRepository(context);
        repository.All().Count().Should().Be(2);
    }
}

Yukarıda görüldüğü üzre nuget'ten eklediğimiz kütüphane ile birlikte DbContextOptionsBuilder sınfınının instance'ını alarak extension metot olarak kullanabileceğimiz UseInMemoryDatabase() metodu yer almakta. Bu metot unit test run edilirken bizim dbContext nesnemizle birebir aynı yeni bir in-memory CustomerDbContext sınıfı oluşturmamıza olanak sağlar. CustomerRepositoryTests sınıfının bütün test metotları ile birlikte son hali aşağıdaki gibidir.

    public class CustomerRepositoryTests
    {
        [Fact]
        public void Save_Should_Save_The_Customer_And_Should_Return_All_Count_As_Two()
        {
            var customer1 = new Domain.Customer("Caner Tosuner", "IST", DateTime.Today.AddYears(28));
            var customer2 = new Domain.Customer("Caner Tosuner", "IST", DateTime.Today.AddYears(28));

            var options = new DbContextOptionsBuilder<CustomerDbContext>()
                .UseInMemoryDatabase("customer_db")
                .Options;

            using (var context = new CustomerDbContext(options))
            {
                var repository = new CustomerRepository(context);
                repository.Save(customer1);
                repository.Save(customer2);
                context.SaveChanges();
            }

            using (var context = new CustomerDbContext(options))
            {
                var repository = new CustomerRepository(context);
                repository.All().Count().Should().Be(2);
            }
        }

        [Fact]
        public void Delete_Should_Delete_The_Customer_And_Should_Return_All_Count_As_One()
        {
            var customer1 = new Domain.Customer("Caner Tosuner", "IST", DateTime.Today.AddYears(28));
            var customer2 = new Domain.Customer("Caner Tosuner", "IST", DateTime.Today.AddYears(28));

            var options = new DbContextOptionsBuilder<CustomerDbContext>()
                .UseInMemoryDatabase("customer_db")
                .Options;

            using (var context = new CustomerDbContext(options))
            {
                var repository = new CustomerRepository(context);
                repository.Save(customer1);
                repository.Save(customer2);
                context.SaveChanges();
            }

            using (var context = new CustomerDbContext(options))
            {
                var repository = new CustomerRepository(context);
                repository.Delete(customer1.Id);
                context.SaveChanges();
            }

            using (var context = new CustomerDbContext(options))
            {
                var repository = new CustomerRepository(context);
                repository.All().Count().Should().Be(1);
            }
        }

        [Fact]
        public void Update_Should_Update_The_Customer()
        {
            var customer = new Domain.Customer("Caner Tosuner", "IST", DateTime.Today.AddYears(28));

            var options = new DbContextOptionsBuilder<CustomerDbContext>()
                .UseInMemoryDatabase("customer_db")
                .Options;

            using (var context = new CustomerDbContext(options))
            {
                var repository = new CustomerRepository(context);
                repository.Save(customer);
                context.SaveChanges();
            }

            customer.SetFields("Caner T", "IZM", customer.BirthDate);

            using (var context = new CustomerDbContext(options))
            {
                var repository = new CustomerRepository(context);
                repository.Update(customer);
                context.SaveChanges();
            }

            using (var context = new CustomerDbContext(options))
            {
                var repository = new CustomerRepository(context);
                var result = repository.Get(customer.Id);

                result.Should().NotBe(null);
                result.FullName.Should().Be(customer.FullName);
                result.CityCode.Should().Be(customer.CityCode);
                result.BirthDate.Should().Be(customer.BirthDate);
            }
        }

        [Fact]
        public void Find_Should_Find_The_Customer_And_Should_Return_All_Count_As_One()
        {
            var customer1 = new Domain.Customer("Caner Tosuner", "IST", DateTime.Today.AddYears(28));
            var customer2 = new Domain.Customer("Caner Tosuner", "IZM", DateTime.Today.AddYears(28));

            var options = new DbContextOptionsBuilder<CustomerDbContext>()
                .UseInMemoryDatabase("customer_db")
                .Options;

            using (var context = new CustomerDbContext(options))
            {
                var repository = new CustomerRepository(context);
                repository.Save(customer1);
                repository.Save(customer2);
                context.SaveChanges();
            }

            using (var context = new CustomerDbContext(options))
            {
                var repository = new CustomerRepository(context);
                var result = repository.Find(c => c.CityCode == customer1.CityCode);
                result.Should().NotBeNull();
                result.Count().Should().Be(1);
            }
        }
    }

Testlerimizi run ettiğimizde aşağıdaki gibi bütün repsoitroy metotlarına ait testlerin success olduğunu görebiliriz.

Source Code

Asp.​Net Core Unit Test Nedir Nasıl Yazılır

Daha önceki unit test yazılarında .net framework uygulamaları için çeşitli kütüphaneler kullanarak unit test nedir ne şekilde neden yazılır gibi konulara değinmiştik. Tekrar kısaca tanımlamak gerekirse; unit test'i yazmış olduğunuz kodun her bir birimi için testler yazarak o kodu veya business'ı test etmek olarak düşünebiliriz. Diğer bir değişle; yazmış olduğumuz sınıf veya metotlara real-case'de gelebilecek olan parametreleri geçerek doğru bir şekilde çalışıp çalışmadığını kontrol etmektir diyebiliriz. Bu yazıda ise asp.net core uygulamalarında unit test yazmak için neler yapmak gerekiyor örnek bir proje üzerinden inceleyeceğiz.

Çok da uzak olmayan bir süre önce .net core 2.1 release oldu ve 2.0'dan sonra gücüne güç katarak emin adımlarla ilerleyeceğini sunduğu benchmark sonuçları ile bizlere göstermiş oldu desek yalan olmaz. Tabi geliştirmiş olduğumuz bu core uygulamalarına test yazmadan olmaz. Asp.Net Core için unit test yazmada kullanılabilecek kütüphaneler nedir diye baktığımızda .net framework'den de hatırlayacağımız Xunit, Moq ve FluentAssertions benzeri kütüphaneler karışımıza çıkmakta. Bu kütüphaneleri kullanarak geliştirdiğimiz .net core uygulamalarına ait sınıflar ve metotlar için unit test yazmak oldukça kolay ve eğlenceli.

Örneğimize başlamadan önce terminolojide bulunan bazı terimlere değinmek gerekirse;

Sut (Service Under Test)

Sut "Service Under Test"'in kısaltılmışı olarak unit test metotlarında test etmek istediğimiz sınıfın&service'in ismini belirtmek için değişken tanımlamada kullanılan kısaltmadır diyebiliriz.

Mocking

Sut içerisinde bulunan business'a ait testleri yazarken içerisinde kullanılan nesnelere ait fake sınıflardır. Bu sınıfların ürettiği process'e göre unit testini yazdığımız business'ın her bir koşuluna göre assertion'lar oluşturabiliriz. Örnek olarak; bir dış servise bağlanıp geriye object return eden bir metodunuz olsun. Siz bu metoda ait unit test yazarken bu dış service gitmek yerine tıpkı o dış service'e gidip response almış veya alamamış gibi bu dış service bağlantısını mock'lamak olarak düşünebilirsiniz.

Expected ve Actual Kavramları

Expected ; unit test yazdığımız fonksiyonalitenin vermesi beklenen çıktısı, result'ını belirtirken kullanılır. Diğer bir değişyle; bu metot veya sınıf bu parametrelerle bu sonucu üretmesi beklenir.

Actual ; unit test'ini yazdığımız metot yada sınıfın gerçek, o an döndüğü result'ı tanımlarken kullanılır.

Assertion

Actual ve Expected değerlerini karşılaştırırken içerisinde tanımlamalar yapabildiğimiz yapının/metodun/sınıfın ismidir.

Yazımızda örnek olarak CustomerApi adındaki web api projemiz için hem controller hemde service katmanları için unit test projeleri oluşturacağız. Hiç vakit kaybetmeden vs'da Customer.Api adında bir asp.net core web api projesi oluştralım. Benim kullandığım environment'da .net core sdk 2.1 yüklü bu yüzden projeleri 2.1 olarak oluşturacağım. Sizlerinde örneği takip edebilmek adına geliştirme ortamınızda .net core 2.0 ve üzeri bir sdk yüklü olması gerekmekte.

Solution açıldıktan sonra birde Customer.Service adında service layer için Core Class Library projesi oluşturalım ve geliştirmelerimize başlayalım.

İlk olarak Customer domain nesnesini oluşturalım. Bu nesne üzerinde Id, FullName, CityCode ve BirthDate alanlarını tutalım. Yeni bir customer yaratırken ve mevcut customer'ı update ederken ilgili validation'ları bu sınıf içerisinde aşağıdaki gibi tanımlayalım.

    public class Customer : Entity
    {
        public string FullName { get; protected set; }
        public string CityCode { get; protected set; }
        public DateTime BirthDate { get; protected set; }

        public Customer(string fullName, string cityCode, DateTime birthDate)
        {
            if (
                string.IsNullOrEmpty(fullName) ||
                string.IsNullOrEmpty(cityCode) ||
                birthDate.Date == DateTime.Today)
            {
                throw new Exception("Fields are not valid to create a new customer.");
            }

            FullName = fullName;
            CityCode = cityCode;
            BirthDate = birthDate;
        }

        protected Customer()
        {
            
        }

        public void SetFields(string fullName, string cityCode, DateTime birthDate)
        {
            if (string.IsNullOrEmpty(fullName) ||
                string.IsNullOrEmpty(cityCode) ||
                birthDate.Date == DateTime.Today)
            {
                throw new Exception("Fields are not valid to update.");
            }

            FullName = fullName;
            CityCode = cityCode;
            BirthDate = birthDate;
        }
    }

Sonrasında Controller'ın doğrudan iletişim kurabildiği ICustomerService ve onun implementasyonunu içeren sınıfları aşağıdaki gibi tanımlayalım. Bu katman CustomerDbContext üzerinden crud işlemlerinin yapılabildiği customerRepository sınıfının kullanıldığı service katmanıdır.

public interface ICustomerService
{
    void CreateNew(CustomerDto customer);
    CustomerDto Update(CustomerDto customer);
    List<CustomerDto> GetAll();
    List<CustomerDto> GetByCityCode(string cityCode);
    CustomerDto GetById(Guid id);
}
    public class CustomerService : ICustomerService
    {
        private readonly ICustomerRepository _customerRepository;
        private readonly ICustomerAssembler _customerAssembler;
        public CustomerService(ICustomerRepository customerRepository, ICustomerAssembler customerAssembler)
        {
            _customerRepository = customerRepository;
            _customerAssembler = customerAssembler;
        }

        public void CreateNew(CustomerDto customerDto)
        {
            var customer = _customerAssembler.ToCustomer(customerDto);

            _customerRepository.Save(customer);
        }

        public CustomerDto Update(CustomerDto customer)
        {
            var existing = _customerRepository.Get(customer.Id);

            existing.SetFields(customer.FullName, customer.CityCode, customer.BirthDate);

            _customerRepository.Update(existing);

            var customerDto = _customerAssembler.ToCustomerDto(existing);

            return customerDto;
        }

        public List<CustomerDto> GetAll()
        {
            var all = _customerRepository.All().ToList();
            return _customerAssembler.ToCustomerDtoList(all);
        }

        public List<CustomerDto> GetByCityCode(string cityCode)
        {
            var list = _customerRepository.Find(c => c.CityCode == cityCode).ToList();
            return _customerAssembler.ToCustomerDtoList(list);
        }

        public CustomerDto GetById(Guid id)
        {
            var customer = _customerRepository.Get(id);
            if (customer == null)
            {
                throw new Exception("Customer with this id : " + id + " not found.");
            }
            var customerDto = _customerAssembler.ToCustomerDto(customer);
            return customerDto;
        }
    }

Yukarıdaki bağımlılığı ve dbContext registration'ı .net core built-in container'a inject etmemiz gerekiyor bunun için Startup.cs içerisinde aşağıdaki gibi bağımlıkları register edelim.

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<CustomerDbContext>(options =>
                options.UseSqlServer(Configuration.GetSection("CustomerDbConnString").Value));

            services.AddScoped<ICustomerRepository, CustomerRepository>();
            services.AddScoped<ICustomerService, CustomerService>();

            services.AddTransient<ICustomerAssembler, CustomerAssembler>();
            services.AddTransient<ICustomerService, CustomerService>();

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        }

Bu service metotlarını kullanacak olan api projemiz ile ilgili son olarak ise CustomerController.cs adında bir api controller oluşturup içerisinde service sınıfında yer alan metotları kullandığımız api end-point'lerini tanımlayalım.

    [Route("api/[controller]")]
    [ApiController]
    public class CustomerController : ControllerBase
    {
        private readonly ICustomerService _customerService;
        public CustomerController(ICustomerService customerService)
        {
            _customerService = customerService;
        }

        // GET api/customer
        [HttpGet]
        public ActionResult<List<CustomerDto>> Get()
        {
            return Ok(_customerService.GetAll());
        }

        // GET api/customer/id
        [HttpGet("{id}")]
        public ActionResult<CustomerDto> Get(Guid id)
        {
            return Ok(_customerService.GetById(id));
        }

        // POST api/customer
        [HttpPost]
        public ActionResult Post([FromBody] CustomerDto customer)
        {
            _customerService.CreateNew(customer);
            return Ok();
        }

        // PUT api/customer
        [HttpPut]
        public ActionResult<CustomerDto> Put([FromBody] CustomerDto customer)
        {
            return Ok(_customerService.Update(customer));
        }

        // GET api/customer/getbycitycode/cityCode
        [HttpGet("getbycitycode/{cityCode}")]
        public ActionResult<List<CustomerDto>> GetByCityCode(string cityCode)
        {
            return Ok(_customerService.GetByCityCode(cityCode));
        }
    }

Projemiz hazır durumda. Şimdi ufaktan unit-tes projelerini oluşturmaya başlayalım.

İlk olarak Service ve Domain sınıfları için solution'a sağ tıklayıp Add-New Project seçeneği seçip aşağıdaki gibi Customer.Service.Test adında bir xUnit Test projesi oluşturalım.

Projemiz bir xUnit test projesidir ve testleri yazarken kullanacağımız bazı kütphaneler şu şekildedir;

  • XUnit
  • Moq
  • FluentAssertions 
  • AutoFixture

dır. Sırasıyla bu kütüphaneleri projemiz referanslarına nuget üzerinden ekleyelim. Paketleri ekledikten sonra ilk olarak Customer domain'i ile test yazmaya başlayalım. Customer sınıfına ait bir instance oluştururken Customer'a ait property'leri update ederken belli bazı kontroller bulunmakta. Bu kontroller için aşağıdaki gibi CustomerTests.cs adında olan sınıf içerisinde unit testleri tanımlayalım.

    public class CustomerTests
    {
        [Theory, AutoMoqData]
        public void Create_Customer_Should_Throw_Exception_When_FullName_Is_Empty(string cityCode, DateTime birthDate)
        {
            Assert.Throws<Exception>(() => new Customer(string.Empty, cityCode, birthDate));
        }

        [Theory, AutoMoqData]
        public void Create_Customer_Should_Throw_Exception_When_CityCode_Is_Empty(string fullName, DateTime birthDate)
        {
            Assert.Throws<Exception>(() => new Domain.Customer(fullName, string.Empty, birthDate));
        }

        [Theory, AutoMoqData]
        public void Create_Customer_Should_Throw_Exception_When_BirthDate_Is_Invalid(string fullName, string cityCode)
        {
            Assert.Throws<Exception>(() => new Domain.Customer(fullName, cityCode, DateTime.Today));
        }

        [Theory, AutoMoqData]
        public void Create_Customer_Should_Success(string fullName, string cityCode, DateTime birthDate)
        {
            var sut = new Domain.Customer(fullName, cityCode, birthDate);

            sut.FullName.Should().Be(fullName);
            sut.CityCode.Should().Be(cityCode);
            sut.BirthDate.Should().Be(birthDate);
        }

        [Theory, AutoMoqData]
        public void SetFields_Should_Update_Fields(string fullName, string cityCode, DateTime birthDate, Domain.Customer sut)
        {
            sut.SetFields(fullName, cityCode, birthDate);

            sut.FullName.Should().Be(fullName);
            sut.CityCode.Should().Be(cityCode);
            sut.BirthDate.Should().Be(birthDate);
        }
    }
    
   //Method parameter olarak Automoq yapabilmek için kullanacağımız attribute
    public class AutoMoqDataAttribute : AutoDataAttribute
    {
        public AutoMoqDataAttribute()
            : base(new Fixture().Customize(new AutoMoqCustomization()))
        {
        }
    }

Yukarıda yazmış olduğumuz testleri run etmek için ise vs. üzerinde Test => Run => All Test diyerek aşağıda olduğu gibi Test Explorer'da Customer sınıfına ait testlerinizin Passed olduğunu görebilirsiniz.

Diğer bir test sınıfı ise ICustomerService interface'ine ait metotları test edebilmek için oluşturup test case'lerini aşağıdaki gibi yazalım.

    public class CustomerServiceTests
    {
        [Theory, AutoMoqData]
        public void CreateNewCustomer_Should_Success([Frozen]Mock<ICustomerAssembler> assembler, [Frozen]Mock<ICustomerRepository> repository, CustomerDto customerDto, Domain.Customer customer, CustomerService sut)
        {
            assembler.Setup(c => c.ToCustomer(customerDto)).Returns(customer);
            repository.Setup(c => c.Save(customer)).Returns(It.IsAny<Guid>());

            Action action = () =>
            {
                sut.CreateNew(customerDto);
            };
            action.Should().NotThrow<Exception>();
        }

        [Theory, AutoMoqData]
        public void UpdateCustomer_Should_Success([Frozen]Mock<ICustomerAssembler> assembler, [Frozen]Mock<ICustomerRepository> repository, CustomerDto customerDto, Domain.Customer customer, CustomerService sut)
        {
            assembler.Setup(c => c.ToCustomer(customerDto)).Returns(customer);
            repository.Setup(c => c.Update(customer));

            Action action = () =>
            {
                sut.Update(customerDto);
            };
            action.Should().NotThrow<Exception>();
        }

        [Theory, AutoMoqData]
        public void GetAll_Should_Success([Frozen]Mock<ICustomerAssembler> assembler, [Frozen]Mock<ICustomerRepository> repository, List<Domain.Customer> customers, List<CustomerDto> customersDtos, CustomerService sut)
        {
            repository.Setup(c => c.All()).Returns(customers.AsQueryable);
            assembler.Setup(c => c.ToCustomerDtoList(customers)).Returns(customersDtos);

            Action action = () =>
            {
                var result = sut.GetAll();
                result.Count.Should().Be(customersDtos.Count);
            };
            action.Should().NotThrow<Exception>();
        }


        [Theory, AutoMoqData]
        public void GetByCityCode_Should_Success([Frozen]Mock<ICustomerAssembler> assembler, [Frozen]Mock<ICustomerRepository> repository, string cityCode, List<Domain.Customer> customers, List<CustomerDto> customersDtos, CustomerService sut)
        {
            assembler.Setup(c => c.ToCustomerDtoList(customers)).Returns(customersDtos);
            repository.Setup(x => x.Find(It.IsAny<Expression<Func<Domain.Customer, bool>>>())).Returns(customers.AsQueryable);

            Action action = () =>
            {
                var result = sut.GetByCityCode(cityCode);
                result.Should().BeEquivalentTo(customersDtos);
            };
            action.Should().NotThrow<Exception>();
        }

        [Theory, AutoMoqData]
        public void GetById_Should_Return_As_Expected([Frozen]Mock<ICustomerAssembler> assembler, [Frozen]Mock<ICustomerRepository> repository, Guid id, CustomerDto customerDto, Domain.Customer customer, CustomerService sut)
        {
            assembler.Setup(c => c.ToCustomerDto(customer)).Returns(customerDto);
            repository.Setup(c => c.Get(id)).Returns(customer);

            Action action = () =>
            {
                var result = sut.GetById(id);
                result.Should().BeEquivalentTo(customerDto);
            };
            action.Should().NotThrow<Exception>();
        }
    }

Domain ve Service sınıfları için unit testlerimizi yukarıdaki gibi oluşturduk. Şimdi ise son olarak Controller için test projesi oluşturup ilgili test case'lerini yazalım. Yine yukarıda olduğu gibi solution'da bir tane Customer.Api.Test adında core xUnit test projesi oluşturalım ve XUnit, Moq, FluentAssertions, AutoFixture kütüphanelerini nuget üzerinden projemize ekleyelim.

    public class CustomerControllerTests
    {
        [Theory, AutoMoqData]
        public void GetAll_Should_Return_As_Expected(Mock<ICustomerService> customerServiceMock, List<CustomerDto> expected)
        {
            var sut = new CustomerController(customerServiceMock.Object);
            customerServiceMock.Setup(c => c.GetAll()).Returns(expected);

            var result = sut.Get();

            var apiOkResult = result.Result.Should().BeOfType<OkObjectResult>().Subject;
            var actual = apiOkResult.Value.Should().BeAssignableTo<List<CustomerDto>>().Subject;

            Assert.Equal(expected, actual);
        }

        [Theory, AutoMoqData]
        public void GetById_Should_Return_As_Expected(Mock<ICustomerService> customerServiceMock, Guid id, CustomerDto expected)
        {
            var sut = new CustomerController(customerServiceMock.Object);
            customerServiceMock.Setup(c => c.GetById(id)).Returns(expected);

            var result = sut.Get(id);

            var apiOkResult = result.Result.Should().BeOfType<OkObjectResult>().Subject;
            var actual = apiOkResult.Value.Should().BeAssignableTo<CustomerDto>().Subject;

            Assert.Equal(expected, actual);
        }

        [Theory, AutoMoqData]
        public void Post_Should_Return_As_Expected(Mock<ICustomerService> customerServiceMock, CustomerDto customer)
        {
            var sut = new CustomerController(customerServiceMock.Object);
            customerServiceMock.Setup(c => c.CreateNew(customer));

            var actual = sut.Post(customer);

            actual.GetType().Should().Be(typeof(OkResult));
        }

        [Theory, AutoMoqData]
        public void Put_Should_Return_As_Expected(Mock<ICustomerService> customerServiceMock, CustomerDto expected)
        {
            var sut = new CustomerController(customerServiceMock.Object);
            customerServiceMock.Setup(c => c.Update(expected)).Returns(expected);

            var result = sut.Put(expected);

            var apiOkResult = result.Result.Should().BeOfType<OkObjectResult>().Subject;
            var actual = apiOkResult.Value.Should().BeAssignableTo<CustomerDto>().Subject;

            Assert.Equal(expected, actual);
        }

        [Theory, AutoMoqData]
        public void GetByCityCode_Should_Return_As_Expected(Mock<ICustomerService> customerServiceMock, string cityCode, List<CustomerDto> expected)
        {
            var sut = new CustomerController(customerServiceMock.Object);
            customerServiceMock.Setup(c => c.GetByCityCode(cityCode)).Returns(expected);

            var result = sut.GetByCityCode(cityCode);

            var apiOkResult = result.Result.Should().BeOfType<OkObjectResult>().Subject;
            var actual = apiOkResult.Value.Should().BeAssignableTo<List<CustomerDto>>().Subject;

            Assert.Equal(expected, actual);
        }
    }

Controller için unit testlerimiz bitti. Şimdi solution'da bulunan bütün testleri run ettiğimizde aşağıdaki gibi hepsinin Passed olduğunu görmemiz gerekir. Toplamda 21 tane unit test metodu bulunmakta ve hepsi geçmiş durumda.

Unit test yazmak oldukça önemli ve yılmadan usanmadan keyif alarak yapılması gereken bir gerçek olarak biz developer'ların hayatında bulunmakta. Bazen unit test yazmak istediğiniz service'i geliştirirken daha az zaman harcamış olduğunuz anlar bile yaşanacaktır ancak titizlikle yazılan her bir unit-test'in bize getirisi oldukça fazla olacaktır. Production'ına çıkmadan fonksiyonaliteyi test etmek olsun, arada kaçak küçek saçma sapan bug'ların önüne geçmek açısından olsun, hemde isimlendirmeleri doğru yaptığımız taktirde projeye yeni başlayan birinin business'ı daha kolay anlayabilmesi gibi durumları göz önüne alırsak unit test yazmanın bir çok faydalı noktası bulunmakta. Üşenmeden gücenmeden yazmanız dileğiyle..

Source Code

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.

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. 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 durdurabilir. Böylelikle unit test yazmak artık yazılım geliştirme dünyasında Nice to Have olmaktan çıkıp Is Must haline 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 bloğudur ve esas olarak kodun veya fonksiyonun belirli bir biriminin davranışını kontrol eder ve geliştirdiğiniz kod dizinindeki akışa dayalı olarak acceptance criteria'ları cover ediyor mu etmiyor mu ölçebilmek adına da büyük öneme sahiptir.

Hiçbir kod ilelebet el değmeden yaşamayacaktır, her zaman için sizden sonra gelen bir developer extra feature'lar ekleyip commit edecektir yada o kodları kullanacak başka ekiplerde olacaktır. Unit test aynı zamanda kodlarınızı kullanacak diğer developer'lar tarafından okunduğunda 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. Yada daha önce yazılmış bir kod bloğuna küçük bir özellik eklemek istiyorsunuz ama bu mevcut kullanıcıları/sistemi bozar mı bozmaz mı diye sorgulamak yerine eğer o kod bloğunun daha önceden yazılmış ve her bir satırını cover eden unit-test metotları var ise tek yapmanız gereken o testleri run edip geçip geçmediğini görmek. Eğer o sonradan yaptığınız geliştirme mevcut'u bozmuyorsa test'ler zaten green statüsünde passed olacaktır.

Unit yazabilmek için nuget üzerinden de install edebildiğimiz bir çok kütüphane bulunmakta.

Bazı Unit Test Frameworkleri

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

 Herhangi birini seçip kendi sitelerinde yayınlamış oldukları sample projeler üzerinden kendi unit test projenizi oluşturabilirsiniz. 

Ö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);
        }
    }

Yazının başında da belirttiğimiz üzre unit-test yazmak ilk başlarda maliyet olarak görünsede aslında oldukça önemli ve hayat kurtarıcı yanları da olabilmekte. Çeşitli framework'ler 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 ms'in burada bulunan adresinden de bulabilirsiniz.