İLERİ
Testing with EF Core
EF Core kullanan kodu test etmek için üç yaklaşım var: SQLite In-Memory (hızlı, çoğu senaryo için yeterli), TestContainers (gerçek DB, en güvenilir), ve InMemory provider (artık önerilmiyor).
Bu bölümde: SQLite In-Memory · TestContainers · Respawn · WebApplicationFactory · Builder Pattern · Bogus
Yaklaşım 1: SQLite In-Memory (Önerilen Başlangıç)
// Test fixture
public class DatabaseFixture : IDisposable
{
public AppDbContext Context { get; }
private readonly SqliteConnection _connection;
public DatabaseFixture()
{
_connection = new SqliteConnection("DataSource=:memory:");
_connection.Open();
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlite(_connection)
.Options;
Context = new AppDbContext(options);
Context.Database.EnsureCreated();
// Test verisi
SeedTestData();
}
private void SeedTestData()
{
Context.Categories.AddRange(
new Category { Id = 1, Name = "Elektronik" },
new Category { Id = 2, Name = "Giyim" }
);
Context.Products.AddRange(
new Product { Id = 1, Name = "Laptop", Price = 25000, CategoryId = 1 },
new Product { Id = 2, Name = "Telefon", Price = 15000, CategoryId = 1 }
);
Context.SaveChanges();
}
public void Dispose()
{
Context.Dispose();
_connection.Dispose();
}
}
Test Sınıfı
public class ProductServiceTests : IClassFixture<DatabaseFixture>
{
private readonly AppDbContext _context;
public ProductServiceTests(DatabaseFixture fixture)
{
_context = fixture.Context;
}
[Fact]
public async Task GetActiveProducts_ShouldReturnOnlyActive()
{
// Arrange
var service = new ProductService(_context);
// Act
var products = await service.GetActiveProductsAsync();
// Assert
Assert.All(products, p => Assert.True(p.IsActive));
}
[Fact]
public async Task GetByCategory_ShouldFilterCorrectly()
{
var service = new ProductService(_context);
var electronics = await service.GetByCategoryAsync(1);
Assert.Equal(2, electronics.Count);
Assert.All(electronics, p => Assert.Equal(1, p.CategoryId));
}
}
Yaklaşım 2: TestContainers (Gerçek SQL Server)
// NuGet: Testcontainers.MsSql
public class SqlServerFixture : IAsyncLifetime
{
private readonly MsSqlContainer _container = new MsSqlBuilder()
.WithImage("mcr.microsoft.com/mssql/server:2022-latest")
.Build();
public AppDbContext Context { get; private set; } = null!;
public async Task InitializeAsync()
{
await _container.StartAsync();
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlServer(_container.GetConnectionString())
.Options;
Context = new AppDbContext(options);
await Context.Database.MigrateAsync();
}
public async Task DisposeAsync()
{
await Context.DisposeAsync();
await _container.DisposeAsync();
}
}
Repository Test Pattern
// Her test temiz veritabanı istiyorsa
public class ProductRepositoryTests : IAsyncLifetime
{
private AppDbContext _context = null!;
private SqliteConnection _connection = null!;
public async Task InitializeAsync()
{
_connection = new SqliteConnection("DataSource=:memory:");
await _connection.OpenAsync();
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlite(_connection)
.Options;
_context = new AppDbContext(options);
await _context.Database.EnsureCreatedAsync();
}
public async Task DisposeAsync()
{
await _context.DisposeAsync();
await _connection.DisposeAsync();
}
[Fact]
public async Task Add_ShouldPersistProduct()
{
// Arrange
var product = new Product { Name = "Test", Price = 100, CategoryId = 1 };
// Act
_context.Products.Add(product);
await _context.SaveChangesAsync();
// Assert
var saved = await _context.Products.FindAsync(product.Id);
Assert.NotNull(saved);
Assert.Equal("Test", saved.Name);
}
}
Test Yaklaşımı Karşılaştırma
| Kriter | InMemory | SQLite | TestContainers |
|---|---|---|---|
| Hız | Çok hızlı | Hızlı | Yavaş (container spin-up) |
| SQL uyumluluğu | Farklı davranır | Çoğu uyumlu | Gerçek SQL Server |
| Transaction test | Desteklemez | Kısmen | Tam destek |
| Migration test | Desteklemez | EnsureCreated | Gerçek migration |
| CI/CD | Kolay | Kolay | Docker gerekir |
| Provider-specific (JSON, Temporal) | Yok | Yok | Tam destek |
Strateji: Birim testler → SQLite. Entegrasyon testleri → TestContainers.
InMemory artık önerilmiyor — gerçek DB davranışını yansıtmıyor.
Respawn ile Test Arası Data Cleanup
// NuGet: Respawn
private Respawner _respawner = null!;
public async Task InitializeAsync()
{
await _container.StartAsync();
// ... migrate ...
_respawner = await Respawner.CreateAsync(ConnectionString, new RespawnerOptions
{
TablesToIgnore = new[] { new Table("__EFMigrationsHistory") },
DbAdapter = DbAdapter.SqlServer // veya DbAdapter.Postgres
});
}
// Her test sonrası temizlik:
public async Task ResetDatabase() => await _respawner.ResetAsync(ConnectionString);
WebApplicationFactory ile API Integration Test
public class ApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public ApiTests(WebApplicationFactory<Program> factory)
{
_client = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Production DB'yi test container ile değiştir
services.RemoveAll<DbContextOptions<AppDbContext>>();
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(TestConnectionString));
});
}).CreateClient();
}
[Fact]
public async Task GetProducts_ReturnsOk()
{
var response = await _client.GetAsync("/api/products");
response.EnsureSuccessStatusCode();
}
}
Test Data Stratejileri
| Yaklaşım | Ne Zaman | Avantaj |
|---|---|---|
| Manual object init | Basit testler, 1-2 entity | Açık, izlenebilir |
| Builder Pattern | Domain nesneleri karmaşık | Fluent API, default'lar kolay |
| Bogus / AutoFixture | Çok sayıda random veri | Hızlı, gerçekçi fake data |
| SQL seed script | Karmaşık ilişkisel veri | DB state garantili |
// Builder Pattern — Test Entity Factory
public class ProductBuilder
{
private string _name = "Test Product";
private decimal _price = 99.99m;
private int _categoryId = 1;
private bool _isActive = true;
public ProductBuilder WithName(string name) { _name = name; return this; }
public ProductBuilder WithPrice(decimal price) { _price = price; return this; }
public ProductBuilder WithCategory(int id) { _categoryId = id; return this; }
public ProductBuilder Inactive() { _isActive = false; return this; }
public Product Build() => new()
{
Name = _name,
Price = _price,
CategoryId = _categoryId,
IsActive = _isActive
};
}
// Kullanım:
var product = new ProductBuilder().WithPrice(250).Inactive().Build();
// Bogus ile tutarlı random test verisi
// NuGet: Bogus
var faker = new Faker<Product>()
.RuleFor(p => p.Name, f => f.Commerce.ProductName())
.RuleFor(p => p.Price, f => f.Random.Decimal(10, 5000))
.RuleFor(p => p.Sku, f => f.Random.AlphaNumeric(10).ToUpper())
.RuleFor(p => p.IsActive, f => f.Random.Bool(0.9f));
// Aynı seed ile her çalıştırmada aynı veri:
var products = faker.UseSeed(42).Generate(50);
Test piramidi: Unit test (repository logic, value objects) → Integration test (DB + EF) → API test (full stack). EF Core testlerinin çoğu integration seviyesinde olmalı.