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