Üretim veritabanı sisteminiz olmadan test etme
Bu sayfada, veritabanınızı bir test dublörüile değiştirerek, uygulamanın üretimde çalıştığı veritabanı sistemini içermeyen otomatik testler yazma tekniklerini açıklıyoruz. Bunu yapmak için çeşitli test dublörleri ve yaklaşım türleri vardır; farklı seçenekleri tam olarak anlamak için Test Stratejisi Seçme bölümünü kapsamlı bir şekilde okumanız önerilir. Son olarak, üretim veritabanı sisteminize karşı test etmek de mümkündür; bu,üretim veritabanı sisteminize karşı test
Bahşiş
Bu sayfada xUnit teknikleri gösterilir, ancak NUnitgibi diğer test çerçevelerinde benzer kavramlar vardır.
Depo düzeni
Üretim veritabanı sisteminizi dahil etmeden testler yazmaya karar verdiyseniz, bunu yapmak için önerilen teknik depo düzenidir; bu konuda daha fazla arka plan için bu bölümü konusuna bakın. Depo desenini uygulamanın ilk adımı, EF Core LINQ sorgularınızı ayırıp ayrı bir katmana çıkarmaktır. Bu işlem, daha sonra güdüklenebilir veya simüle edilebilir. Bloglama sistemimiz için bir depo arabirimi örneği aşağıda verilmişti:
public interface IBloggingRepository
{
Task<Blog> GetBlogByNameAsync(string name);
IAsyncEnumerable<Blog> GetAllBlogsAsync();
void AddBlog(Blog blog);
Task SaveChangesAsync();
}
... ve aşağıda üretim kullanımı için kısmi bir örnek uygulama verilmişti:
public class BloggingRepository : IBloggingRepository
{
private readonly BloggingContext _context;
public BloggingRepository(BloggingContext context)
=> _context = context;
public async Task<Blog> GetBlogByNameAsync(string name)
=> await _context.Blogs.FirstOrDefaultAsync(b => b.Name == name);
// Other code...
}
Çok fazla bir şey yok: depo yalnızca EF Core bağlamını sarmalar ve veritabanı sorgularını yürüten ve bu bağlamı güncelleştiren yöntemleri kullanıma sunar. Önemli bir nokta, GetAllBlogs
yöntemimizin IQueryable<Blog>
değil IAsyncEnumerable<Blog>
(veya IEnumerable<Blog>
) döndürdüğüdür. İkincisini döndürmek, sorgu işleçlerinin sonuç üzerinde oluşturulabilir olması ve EF Core'un sorgunun çevirisine hala dahil olması gerektiği anlamına gelir; bu durum, bir depo bulundurmanın başlangıçtaki amacını boşa çıkarır.
IAsyncEnumerable<Blog>
, deponun döndürdüğü verileri kolayca saptamamıza veya taklit etmemize olanak tanır.
ASP.NET Core uygulaması için, depoyu bağımlılık ekleme sisteminde hizmet olarak kaydetmek amacıyla uygulamanın ConfigureServices
kısmına aşağıdakileri ekleyerek yapmamız gerekir:
services.AddScoped<IBloggingRepository, BloggingRepository>();
Son olarak, denetleyicilerimiz EF Core bağlamı yerine depo hizmetine enjekte edilir ve üzerinde yöntemler uygular:
private readonly IBloggingRepository _repository;
public BloggingControllerWithRepository(IBloggingRepository repository)
=> _repository = repository;
[HttpGet]
public async Task<Blog> GetBlog(string name)
=> await _repository.GetBlogByNameAsync(name);
Bu noktada, uygulamanız depo düzenine göre tasarlanır: veri erişim katmanıyla tek iletişim noktası olan EF Core, artık uygulama kodu ile gerçek veritabanı sorguları arasında bir aracı işlevi gören depo katmanı üzerinden yapılır. Testler artık depoyu sahteleyerek veya en sevdiğiniz mock kütüphanesi ile sahtesini oluşturarak basitçe yazılabilir. Popüler Moq kitaplığını kullanan sahte tabanlı bir test örneği aşağıda verilmişti:
[Fact]
public async Task GetBlog()
{
// Arrange
var repositoryMock = new Mock<IBloggingRepository>();
repositoryMock
.Setup(r => r.GetBlogByNameAsync("Blog2"))
.Returns(Task.FromResult(new Blog { Name = "Blog2", Url = "http://blog2.com" }));
var controller = new BloggingControllerWithRepository(repositoryMock.Object);
// Act
var blog = await controller.GetBlog("Blog2");
// Assert
repositoryMock.Verify(r => r.GetBlogByNameAsync("Blog2"));
Assert.Equal("http://blog2.com", blog.Url);
}
Örnek kodun tamamı burada
Bellek içi SQLite
SQLite, üretim veritabanı sisteminiz (e.g. SQL Server) yerine test paketiniz için EF Core sağlayıcısı olarak kolayca yapılandırılabilir; ayrıntılar için SQLite sağlayıcı belgelerine başvurun. Ancak, testler arasında kolay yalıtım sağladığından ve gerçek SQLite dosyalarıyla ilgilenmeyi gerektirmediğinden, test sırasında SQLite'in bellek içi veritabanı özelliğini kullanmak genellikle iyi bir fikirdir.
Bellek içi SQLite kullanmak için, düşük düzeyli bir bağlantı açıldığında yeni bir veritabanı oluşturulduğunu ve bu bağlantı kapatıldığında silindiğini anlamak önemlidir. Normal kullanımda EF Core'un DbContext
, gereksiz yere uzun süre bağlantının tutulmasını önlemek için gerektiğinde veritabanı bağlantılarını (her sorgu yürütülürken) açar ve kapatır. Ancak, bellek içi SQLite ile bu durum veritabanını her seferinde sıfırlamaya neden olabilir; bu nedenle geçici bir çözüm olarak, bağlantıyı EF Core'a geçirmeden önce açar ve yalnızca test tamamlandığında kapatılmasını düzenleriz:
public SqliteInMemoryBloggingControllerTest()
{
// Create and open a connection. This creates the SQLite in-memory database, which will persist until the connection is closed
// at the end of the test (see Dispose below).
_connection = new SqliteConnection("Filename=:memory:");
_connection.Open();
// These options will be used by the context instances in this test suite, including the connection opened above.
_contextOptions = new DbContextOptionsBuilder<BloggingContext>()
.UseSqlite(_connection)
.Options;
// Create the schema and seed some data
using var context = new BloggingContext(_contextOptions);
if (context.Database.EnsureCreated())
{
using var viewCommand = context.Database.GetDbConnection().CreateCommand();
viewCommand.CommandText = @"
CREATE VIEW AllResources AS
SELECT Url
FROM Blogs;";
viewCommand.ExecuteNonQuery();
}
context.AddRange(
new Blog { Name = "Blog1", Url = "http://blog1.com" },
new Blog { Name = "Blog2", Url = "http://blog2.com" });
context.SaveChanges();
}
BloggingContext CreateContext() => new BloggingContext(_contextOptions);
public void Dispose() => _connection.Dispose();
Testler artık oluşturucuda ayarladığımız bağlantıyı kullanarak bir bağlam döndüren CreateContext
çağırabilir ve çekirdek verileri içeren temiz bir veritabanımız olmasını sağlar.
Bellek içi SQLite testi için tam örnek kod burada
Bellek içi sağlayıcı
public InMemoryBloggingControllerTest()
{
_contextOptions = new DbContextOptionsBuilder<BloggingContext>()
.UseInMemoryDatabase("BloggingControllerTest")
.ConfigureWarnings(b => b.Ignore(InMemoryEventId.TransactionIgnoredWarning))
.Options;
using var context = new BloggingContext(_contextOptions);
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
context.AddRange(
new Blog { Name = "Blog1", Url = "http://blog1.com" },
new Blog { Name = "Blog2", Url = "http://blog2.com" });
context.SaveChanges();
}
Bellek içi test için tam örnek kod burada
Bellek içi veritabanı adlandırma
Bellek içi veritabanları basit bir dize adıyla tanımlanır ve aynı adı sağlayarak aynı veritabanına birkaç kez bağlanmak mümkündür (bu nedenle yukarıdaki örnekte her test öncesinde EnsureDeleted
çağrılmalıdır). Ancak, bellek içi veritabanlarının köklerinin bağlamın iç hizmet sağlayıcısında olduğuna dikkat edin; çoğu durumda bağlamlar aynı hizmet sağlayıcısını paylaşırken, bağlamları farklı seçeneklerle yapılandırmak yeni bir iç hizmet sağlayıcısının kullanımını tetikleyebilir. Bu durumda, bellek içi veritabanlarını paylaşması gereken tüm bağlamlar için UseInMemoryDatabase
'e doğrudan aynı InMemoryDatabaseRoot örneğini gönderin (bu genellikle statik bir InMemoryDatabaseRoot
alanına sahip olarak yapılır).
İşlemler
Varsayılan olarak, bir işlem başlatılırsa, işlemler desteklenmediğinden bellek içi sağlayıcının bir özel durum oluşturacağını unutmayın. Bunun yerine, EF Core'u yukarıdaki örnekte olduğu gibi InMemoryEventId.TransactionIgnoredWarning
'ı yoksayacak şekilde yapılandırarak işlemlerin sessizce göz ardı edilmesini isteyebilirsiniz. Ancak kodunuz işlemsel semantiğe güveniyorsa (örneğin, geri alma işleminin gerçekten değişiklikleri geri almasına bağlıysa) testiniz çalışmaz.
Görüşler
Bellek içi sağlayıcı, ToInMemoryQuerykullanarak LINQ sorguları aracılığıyla görünümlerin tanımlanmasına izin verir:
modelBuilder.Entity<UrlResource>()
.ToInMemoryQuery(() => context.Blogs.Select(b => new UrlResource { Url = b.Url }));