.NET Testing: Unit, Integration ve E2E Test Stratejileri
# .NET Test Stratejileri: Unit, Integration ve E2E
Test yazmak, production hatalarini azaltmanin en etkili yoludur. Ancak onemli olan test sayisi degil, dogru seyleri dogru seviyede test etmektir. Bu rehberde, .NET projelerinde test piramidinin her katmanini gercek kod ornekleriyle anlatiyorum.
Test Piramidi
Test piramidi, hangi testin ne kadar yazilmasi gerektigini gosteren bir modeldir. Tabaninda hizli ve ucuz unit testler, ortasinda entegrasyon testleri, tepesinde az sayida E2E test bulunur.
Unit Testler
Unit testler is mantigi izole olarak dogrular. Hizli calisir, disariya bagimlilik icermez ve bir davranisi belgeler nitelikte olmalidir.
public class SiparisServiceTests
{
private readonly Mock<ISiparisRepository> _siparisRepoMock;
private readonly Mock<IStokService> _stokMock;
private readonly SiparisService _sut;
public SiparisServiceTests()
{
_siparisRepoMock = new Mock<ISiparisRepository>();
_stokMock = new Mock<IStokService>();
_sut = new SiparisService(_siparisRepoMock.Object, _stokMock.Object);
}
[Fact]
public async Task SiparisOlustur_YeterliStokVarsa_SiparisiOnaylamali()
{
class=class="code-string">"code-comment">// Arrange
var talep = new SiparisTalebi(class="code-string">"SKU-class="code-number">001", miktar: class="code-number">3);
_stokMock
.Setup(x => x.StokKontrol(class="code-string">"SKU-class="code-number">001"))
.ReturnsAsync(new StokSonucu(Mevcut: class="code-number">10));
class=class="code-string">"code-comment">// Act
var sonuc = await _sut.SiparisOlusturAsync(talep);
class=class="code-string">"code-comment">// Assert
sonuc.Should().NotBeNull();
sonuc.Durum.Should().Be(SiparisDurumu.Onaylandi);
_siparisRepoMock.Verify(x => x.KaydetAsync(It.IsAny<Siparis>()), Times.Once);
}
[Fact]
public async Task SiparisOlustur_StokYetersizse_StokYokDonmeli()
{
class=class="code-string">"code-comment">// Arrange
var talep = new SiparisTalebi(class="code-string">"SKU-class="code-number">001", miktar: class="code-number">50);
_stokMock
.Setup(x => x.StokKontrol(class="code-string">"SKU-class="code-number">001"))
.ReturnsAsync(new StokSonucu(Mevcut: class="code-number">2));
class=class="code-string">"code-comment">// Act
var sonuc = await _sut.SiparisOlusturAsync(talep);
class=class="code-string">"code-comment">// Assert
sonuc.Durum.Should().Be(SiparisDurumu.StokYok);
_siparisRepoMock.Verify(x => x.KaydetAsync(It.IsAny<Siparis>()), Times.Never);
}
}Unit testlerde dikkat edilmesi gerekenler:
Entegrasyon Testleri
Entegrasyon testleri, bilesenlerin birlikte dogru calistigini dogrular: veritabani mapping'leri, API sozlesmeleri, mesajlasma katmanlari. Unit testlerin yakalayamadigi hatalari yakalamak icin kritiktir.
public class SiparisApiTests : IClassFixture<CustomWebApplicationFactory>
{
private readonly HttpClient _client;
public SiparisApiTests(CustomWebApplicationFactory factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task SiparisOlustur_GeçerliVeriyle_CreatedDonmeli()
{
class=class="code-string">"code-comment">// Arrange
var talep = new { Sku = class="code-string">"SKU-class="code-number">001", Miktar = class="code-number">2 };
var content = new StringContent(
JsonSerializer.Serialize(talep),
Encoding.UTF8,
class="code-string">"application/json");
class=class="code-string">"code-comment">// Act
var response = await _client.PostAsync(class="code-string">"/api/siparisler", content);
class=class="code-string">"code-comment">// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
var body = await response.Content.ReadFromJsonAsync<SiparisResponse>();
body!.Sku.Should().Be(class="code-string">"SKU-class="code-number">001");
body.Durum.Should().Be(class="code-string">"Onaylandi");
}
[Fact]
public async Task SiparisOlustur_GecersizVeriyle_BadRequestDonmeli()
{
class=class="code-string">"code-comment">// Arrange
var talep = new { Sku = class="code-string">"", Miktar = -class="code-number">1 };
var content = new StringContent(
JsonSerializer.Serialize(talep),
Encoding.UTF8,
class="code-string">"application/json");
class=class="code-string">"code-comment">// Act
var response = await _client.PostAsync(class="code-string">"/api/siparisler", content);
class=class="code-string">"code-comment">// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
}
}`WebApplicationFactory` yapilandirmasi:
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
class=class="code-string">"code-comment">// Gercek veritabani contextclass="code-string">'ini kaldir
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
if (descriptor != null) services.Remove(descriptor);
class=class="code-string">"code-comment">// Hizli testler icin in-memory veritabani ekle
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase(class="code-string">"TestDb"));
class=class="code-string">"code-comment">// Dis servisleri fake'lerle degistir
services.AddScoped<IOdemeServisi, FakeOdemeServisi>();
});
}
}Testcontainers ile Entegrasyon Testleri
Gercek bir veritabani motoruna ihtiyac duyan testler icin Testcontainers, Docker container'larini aninda baslatir. Manuel kurulum gerektirmeden production benzeri bir ortam saglar.
public class SiparisRepositoryTests : IAsyncLifetime
{
private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder()
.WithImage(class="code-string">"postgres:class="code-number">16-alpine")
.WithDatabase(class="code-string">"testdb")
.WithUsername(class="code-string">"test")
.WithPassword(class="code-string">"test")
.Build();
private AppDbContext _dbContext = null!;
public async Task InitializeAsync()
{
await _postgres.StartAsync();
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseNpgsql(_postgres.GetConnectionString())
.Options;
_dbContext = new AppDbContext(options);
await _dbContext.Database.MigrateAsync();
}
public async Task DisposeAsync()
{
await _dbContext.DisposeAsync();
await _postgres.DisposeAsync();
}
[Fact]
public async Task SiparisKaydet_VeritabaninaYazmali()
{
class=class="code-string">"code-comment">// Arrange
var repo = new SiparisRepository(_dbContext);
var siparis = new Siparis(class="code-string">"SKU-class="code-number">001", class="code-number">3, SiparisDurumu.Onaylandi);
class=class="code-string">"code-comment">// Act
await repo.KaydetAsync(siparis);
var geriAlınan = await repo.IdIleGetirAsync(siparis.Id);
class=class="code-string">"code-comment">// Assert
geriAlınan.Should().NotBeNull();
geriAlınan!.Sku.Should().Be(class="code-string">"SKU-class="code-number">001");
geriAlınan.Miktar.Should().Be(class="code-number">3);
}
}E2E (Uctan Uca) Testler
E2E testler, kritik kullanici senaryolarini uctan uca dogrular. Tam deploy edilmis bir ortamda calisir. Kapsami dar tutarak kararsiz test suite'lerinden kacinin.
.NET Test Araclari
| Arac | Kullanim Amaci |
|------|---------------|
| xUnit | Paralel calistirma destekli test runner |
| FluentAssertions | Okunabilir, zincirlenebilir assertion'lar |
| Moq | Interface tabanli mock framework |
| NSubstitute | Daha temiz sozdizimli alternatif mock framework |
| Testcontainers | Docker tabanli test altyapisi |
| Bogus | Gercekci sahte veri uretimi |
| Respawn | Testler arasi hizli veritabani temizligi |
| Verify | Karmasik ciktilar icin snapshot testing |
FluentAssertions Kullanimi
FluentAssertions, test hatalarini anlasilir mesajlara donusturur:
class=class="code-string">"code-comment">// Klasik yontem:
Assert.Equal(class="code-string">"Onaylandi", siparis.Durum);
class=class="code-string">"code-comment">// Expected: "Onaylandi", Actual: "Beklemede"
class=class="code-string">"code-comment">// FluentAssertions ile:
siparis.Durum.Should().Be(class="code-string">"Onaylandi");
class=class="code-string">"code-comment">// Expected siparis.Durum to be "Onaylandi", but found "Beklemede".
class=class="code-string">"code-comment">// Koleksiyon assertionclass="code-string">'lari
siparisler.Should().HaveCount(class="code-number">3)
.And.OnlyContain(s => s.Toplam > class="code-number">0)
.And.BeInAscendingOrder(s => s.OlusturmaTarihi);
class=class="code-string">"code-comment">// Exception assertion'lari
var act = () => service.SiparisOlusturAsync(gecersizTalep);
await act.Should().ThrowAsync<ValidationException>()
.WithMessage(class="code-string">"*SKU*zorunlu*");Test Verisi Yonetimi
Sabit kodlanmis test verisi kisa surede bakim yukune donusur. Builder ve fixture'lar kullanarak testleri okunabilir ve sema degisikliklerine dayanikli tutun.
Test Data Builder'lar
public class SiparisBuilder
{
private string _sku = class="code-string">"SKU-class="code-number">001";
private int _miktar = class="code-number">1;
private SiparisDurumu _durum = SiparisDurumu.Beklemede;
private DateTime _olusturmaTarihi = DateTime.UtcNow;
public SiparisBuilder SkuIle(string sku) { _sku = sku; return this; }
public SiparisBuilder MiktarIle(int miktar) { _miktar = miktar; return this; }
public SiparisBuilder DurumIle(SiparisDurumu d) { _durum = d; return this; }
public SiparisBuilder OlusturmaTarihiIle(DateTime dt) { _olusturmaTarihi = dt; return this; }
public Siparis Olustur() => new Siparis(_sku, _miktar, _durum)
{
OlusturmaTarihi = _olusturmaTarihi
};
}
class=class="code-string">"code-comment">// Testlerde kullanim:
var siparis = new SiparisBuilder()
.SkuIle(class="code-string">"SKU-PREMIUM")
.MiktarIle(class="code-number">5)
.DurumIle(SiparisDurumu.Onaylandi)
.Olustur();xUnit ile Paylasilan Fixture'lar
public class VeritabaniFixture : IAsyncLifetime
{
public AppDbContext DbContext { get; private set; } = null!;
private readonly PostgreSqlContainer _container = new PostgreSqlBuilder()
.WithImage(class="code-string">"postgres:class="code-number">16-alpine")
.Build();
public async Task InitializeAsync()
{
await _container.StartAsync();
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseNpgsql(_container.GetConnectionString())
.Options;
DbContext = new AppDbContext(options);
await DbContext.Database.MigrateAsync();
}
public async Task DisposeAsync()
{
await DbContext.DisposeAsync();
await _container.DisposeAsync();
}
}
[CollectionDefinition(class="code-string">"Veritabani")]
public class VeritabaniCollection : ICollectionFixture<VeritabaniFixture> { }
[Collection(class="code-string">"Veritabani")]
public class UrunRepositoryTests
{
private readonly VeritabaniFixture _fixture;
public UrunRepositoryTests(VeritabaniFixture fixture)
{
_fixture = fixture;
}
[Fact]
public async Task TumunuGetir_TohumVerileriDonmeli()
{
var repo = new UrunRepository(_fixture.DbContext);
var urunler = await repo.TumunuGetirAsync();
urunler.Should().NotBeEmpty();
}
}Neyi Test Etmeli, Neyi Atlmali
Her sey test edilmeye deger degildir. Yanlis seyleri test etmek zaman kaybettirir ve guveni artirmadan surtuume yaratir.
Mutlaka Test Edin
Genellikle Atlayin
Gri Alan (Karmasiksa Test Edin)
Kendi .NET servislerimde su test kapsami stratejisini uyguluyorum: Testlerimin %80'i domain ve uygulama mantigini kapsayan unit testler, %15'i Testcontainers ile veritabani ve API katmanini test eden entegrasyon testleri, %5'i kritik kullanici akislari icin E2E smoke testler. Bir kapsam yuzdesini kovalamiyorum; kirildiginda gercek hasar verecek davranislari test etmeye odaklaniyorum.
CI Entegrasyonu
Testler ancak otomatik olarak calistirildiginda deger tasir. Asagida her pull request'te tam test suite'ini calistiran bir GitHub Actions workflow'u bulunuyor.
name: .NET Test Suite
on:
pull_request:
branches: [main, develop]
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: .NET Kur
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Bagimliliklari Yukle
run: dotnet restore
- name: Derle
run: dotnet build --no-restore --configuration Release
- name: Unit Testleri Calistir
run: dotnet test --no-build --configuration Release --filter "Category=Unit" --logger "trx;LogFileName=unit-results.trx"
- name: Entegrasyon Testlerini Calistir
run: dotnet test --no-build --configuration Release --filter "Category=Integration" --logger "trx;LogFileName=integration-results.trx"
env:
ConnectionStrings__Default: "Host=localhost;Port=5432;Database=testdb;Username=test;Password=test"
- name: Test Sonuclarini Yayinla
uses: dorny/test-reporter@v1
if: always()
with:
name: Test Sonuclari
path: '**/*.trx'
reporter: dotnet-trxCI icin Testleri Kategorize Etme
xUnit trait'lerini kullanarak testleri etiketleyin, boylece CI bunlari ayri asamalarda calistirabilir:
[Trait(class="code-string">"Category", class="code-string">"Unit")]
public class FiyatlandirmaServiceTests
{
[Fact]
public void IndirimHesapla_AltinaUye_Yuzde20Donmeli()
{
var service = new FiyatlandirmaService();
var indirim = service.IndirimHesapla(UyelikKatmani.Altin, 100m);
indirim.Should().Be(20m);
}
}
[Trait(class="code-string">"Category", class="code-string">"Integration")]
public class OdemeEntegrasyonTests : IClassFixture<CustomWebApplicationFactory>
{
class=class="code-string">"code-comment">// Altyapi gerektiren entegrasyon testleri
}Yaygin Test Hatalari
.NET projelerinde defalarca gordugum ve kirilgan, yavas veya yaniltici test suite'lerine yol acan kaliplar bunlar.
1. Davranis Yerine Implementasyonu Test Etmek
class=class="code-string">"code-comment">// Kotu: ic metot cagrilarina siki bagli
_repoMock.Verify(x => x.TransactionBaslat(), Times.Once);
_repoMock.Verify(x => x.TransactionOnayla(), Times.Once);
_loggerMock.Verify(x => x.Log(It.IsAny<string>()), Times.Exactly(class="code-number">3));
class=class="code-string">"code-comment">// Iyi: gozlemlenebilir sonucu dogrula
var sonuc = await _sut.OdemeIsleAsync(talep);
sonuc.Basarili.Should().BeTrue();
sonuc.IslemId.Should().NotBeEmpty();2. Testler Arasi Paylasilan Degisen Durum
class=class="code-string">"code-comment">// Kotu: testler calisma sirasina bagimli
private static List<Siparis> _siparisler = new();
[Fact]
public void Test1_SiparisEkle() { _siparisler.Add(new Siparis()); }
[Fact]
public void Test2_SayiKontrol() { _siparisler.Should().HaveCount(class="code-number">1); } class=class="code-string">"code-comment">// Kararsiz!
class=class="code-string">"code-comment">// Iyi: her test kendi durumunu olusturur
[Fact]
public void SiparisEkle_SayiyiArtirmali()
{
var siparisler = new List<Siparis>();
siparisler.Add(new Siparis());
siparisler.Should().HaveCount(class="code-number">1);
}3. Asiri Mock Kullanimi
class=class="code-string">"code-comment">// Kotu: test edilen seyi mockclass="code-string">'lamak
var mockService = new Mock<ISiparisService>();
mockService.Setup(x => x.SiparisOlustur(It.IsAny<Siparis>())).Returns(true);
var sonuc = mockService.Object.SiparisOlustur(new Siparis());
class=class="code-string">"code-comment">// Tebrikler, Moq'u test ettiniz, kodunuzu degil.
class=class="code-string">"code-comment">// Iyi: bagimliliklari mock'la, gercek servisi test et
var repoMock = new Mock<ISiparisRepository>();
var service = new SiparisService(repoMock.Object);
var sonuc = service.SiparisOlustur(new Siparis());4. Hata Yollarini Gormezden Gelmek
Servisiniz exception yakalayip hata sonucu donuyorsa, bu yollari da test edin. Mutlu yol genellikle aciktir; hatalarin gizlendigi yer hata yollarinin icindedir.
5. Dev Arrange Bloklari
Test kurulumunuz 40 satir uzunlugundaysa, bunu bir builder veya fixture'a cikartin. Testler bir bakista okunabilir olmalidir.
6. Test Verisini Temizlememek
Arkasinda veri birakan entegrasyon testleri zincirleme hatalara neden olur. Testler arasi durumu sifirlamak icin `Respawn` veya transaction rollback kullanin:
public class EntegrasyonTestBase : IAsyncLifetime
{
private readonly Respawner _respawner;
protected readonly AppDbContext DbContext;
public async Task InitializeAsync()
{
_respawner = await Respawner.CreateAsync(connectionString, new RespawnerOptions
{
DbAdapter = DbAdapter.Postgres,
SchemasToInclude = new[] { class="code-string">"public" }
});
}
public async Task DisposeAsync()
{
await _respawner.ResetAsync(connectionString);
}
}Sonuc
Guclu bir .NET test stratejisi, is mantigi icin hizli unit testler, gercek altyapiyla anlamli entegrasyon kontrolleri ve az ama kritik uctan uca korumadan olusur. Hedef %100 kapsam degil, onemli davranislara %100 guven duymaktir.
Kendi .NET servislerimde, test altyapisina (builder'lar, fixture'lar, paylasilan factory'ler) yatirim yapmanin buyuk getiri sagladigini gordum. Yazmasi ve bakimi kolay bir test suite'i yazilir. Zahmetli olan ise atlanir.
Test stratejinizi birlikte olusturabiliriz.
İlgili Makaleler
.NET'te Clean Architecture: Ölçeklenebilir Proje Yapısı
.NET projelerinde Clean Architecture uygulayın. Katmanlar, bağımlılık yönetimi ve test edilebilir kod için rehber.
.NET'te Dependency Injection: Temel Kavramlar ve Uygulama
.NET'te Dependency Injection'ı doğru anlayın ve uygulayın. Service lifetimes, registration patterns ve best practices.
.NET CI/CD: GitHub Actions ve Azure DevOps ile Otomatik Deployment
.NET projeleri için CI/CD pipeline tasarlayın. GitHub Actions, Azure DevOps ve deployment stratejileri.
Flutter Projeniz mi Var?
iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.
İletişime Geç