.NET Testing: Unit, Integration ve E2E Test Stratejileri

15 dakika okuma9 Şubat 2026Güncellendi: 9 Mar 2026
.NET testingxUnit tutorialC# unit testingIntegration testing .NETMoq .NETFluentAssertions.NET test best practicesTest pyramid .NET

# .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.

csharp
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:

  • Her test metodu tek bir davranisi dogrulamali
  • Test isimleri senaryoyu acikca tarif etmeli: `MetotAdi_Kosul_BeklenenSonuc`
  • Dissal bagimliliklar mock'lanmali, test edilen sinif degil
  • Private metotlar dogrudan test edilmemeli
  • Entegrasyon Testleri

    Entegrasyon testleri, bilesenlerin birlikte dogru calistigini dogrular: veritabani mapping'leri, API sozlesmeleri, mesajlasma katmanlari. Unit testlerin yakalayamadigi hatalari yakalamak icin kritiktir.

    csharp
    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:

    csharp
    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.

    csharp
    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.

  • E2E testleri 5-10 kritik akisla sinirli tutun (giris, satin alma, odeme)
  • Her commit'te degil, release oncesi veya zamanlanmis olarak calistirin
  • Gecici hatalar icin retry mekanizmasi kullanin ama tekrarlayan retry'lari arastirin
  • .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:

    csharp
    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

    csharp
    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

    csharp
    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

  • **Domain mantigi ve is kurallari** -- uygulamanizin cekirdegi
  • **Validasyon mantigi** -- sinir degerleri, zorunlu alanlar, format kurallari
  • **Kenar durumlari** -- null girdiler, bos koleksiyonlar, tasman senaryolari
  • **Hata yonetimi** -- bir bagimliligin basarisiz oldugu durumlar
  • **Guvenlik akislari** -- kimlik dogrulama, yetkilendirme, girdi temizleme
  • **Veri eslemesi** -- EF Core yapilandirmalari, AutoMapper profilleri
  • Genellikle Atlayin

  • **Framework kodu** -- ASP.NET routing veya EF Core ic mekanizmalarini test etmeyin
  • **Basit getter/setter'lar** -- mantik yoksa test gereksiz
  • **Ucuncu parti kutuphane davranislari** -- kendi test suite'lerine guvenin
  • **UI yerlesim detaylari** -- piksel bazinda konum surekli degisir
  • **Tek satirlik gecis metotlari** -- sadece delege eden metotlar entegrasyon testinde zaten kaplanir
  • Gri Alan (Karmasiksa Test Edin)

  • **Controller'lar** -- ince controller'lara unit test gereksiz, ama entegrasyon testiyle test edin
  • **Konfigarasyon binding** -- config degerlerine bagli kosullu mantik varsa test edin
  • **Loglama** -- yalnizca uyumluluk gereksinimi varsa dogrulayin
  • 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.

    yaml
    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-trx

    CI icin Testleri Kategorize Etme

    xUnit trait'lerini kullanarak testleri etiketleyin, boylece CI bunlari ayri asamalarda calistirabilir:

    csharp
    [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

    csharp
    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

    csharp
    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

    csharp
    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:

    csharp
    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

    Flutter Projeniz mi Var?

    iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.

    İletişime Geç