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 katmanı için entity framework kullanılan bir projede nasıl unit test metotları 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