EFEF Core Handbook

İ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ı.