Mert Özen Her Satırda Daha İleri
Test ve Kalite Unit Test — #13

xUnit v2'den v3'e ve 4.0'a: Eski Test Projelerini Okumak ve Taşımak

Mert Özen 8 Kas 2026 9 dk 3 görüntülenme
xUnit v2'den v3'e ve 4.0'a: Eski Test Projelerini Okumak ve Taşımak

xUnit v2 projesini v3'e taşımak paketleri tek pakete indirmek, projeyi executable yapmak ve birkaç imzayı değiştirmektir; 4.0'a geçişte asıl risk ise derlenen ama davranışı değişen extensibility kodu. 3.x'te yazılmış bir ITestCaseOrderer 4.0.2'de uyarısız derlendi ve method sıralamasını sessizce bıraktı. Gerçek derleme hatalarıyla adım adım taşıma, dotnet new xunit'in hâlâ v2 üretmesi, xunit-console-tool ve bir checklist.

Sürüm notu: Bu yazıdaki kodlar .NET 10, xUnit.net 2.9.3, xUnit.net v3 3.2.2 ve 4.0.2 ile derlenip denenmiştir; konsol runner'ı xunit-console-tool 4.0.2.

Önce Tahmin Et

Bir ekip, xUnit v3'ün 3.x sürümünde testlerini alfabetik sırayla çalıştırmak için özel bir test case orderer yazmış (sıra bağımlı testlerin neden kötü olduğunu 05. yazıda konuştuk, ama bu ekibin eski bir entegrasyon senaryosu var):

using Xunit;
using Xunit.Sdk;
using Xunit.v3;

[assembly: TestCaseOrderer(typeof(Orderer.Tests.AlphabeticalOrderer))]

namespace Orderer.Tests;

// xUnit v3 3.x için yazılmış özel test case orderer
public sealed class AlphabeticalOrderer : ITestCaseOrderer
{
    public IReadOnlyCollection<TTestCase> OrderTestCases<TTestCase>(IReadOnlyCollection<TTestCase> testCases)
        where TTestCase : notnull, ITestCase =>
        testCases.OrderBy(tc => tc.TestMethodName, StringComparer.Ordinal).ToArray();
}

public sealed class OrderedTests
{
    [Fact]
    public void D_fourth() => Console.WriteLine("D_fourth");

    [Fact]
    public void B_second() => Console.WriteLine("B_second");

    [Fact]
    public void C_third() => Console.WriteLine("C_third");

    [Fact]
    public void A_first() => Console.WriteLine("A_first");
}

Paketi xunit.v3 3.2.2'den 4.0.2'ye yükseltiyorlar. Proje derlenir mi? Derlenirse testler hangi sırayla çalışır?

Cevap: Derleniyor, Ama Sıra Kayboluyor

Build uyarısız geçiyor. dotnet run ile iki sürümün çıktısı:

== 3.2.2
A_first
B_second
C_third
D_fourth
   Orderer.Tests  Total: 4, Errors: 0, Failed: 0, Skipped: 0, Not Run: 0, Time: 0.044s
== 4.0.2
B_second
A_first
D_fourth
C_third
   Orderer.Tests  Total: 4, Errors: 0, Failed: 0, Skipped: 0, Not Run: 0, Time: 0.045s

Hiçbir hata, hiçbir uyarı yok; sıralama sessizce kayboldu. 4.0 sıralamayı katmanlara ayırdı: önce collection, sonra class, sonra method, en son test case. ITestCaseOrderer artık yalnız aynı method'un test case'lerini (bir theory'nin satırlarını) sıralıyor; method'ları sıralamak yeni ITestMethodOrderer'ın işi. 4.0 sürüm notu bunu "mevcut ITestCaseOrderer implementation'ları büyük ihtimalle etkilenir" diye uyarıyor. Düzeltilmiş hâli:

[assembly: TestMethodOrderer(typeof(Orderer.Tests.AlphabeticalMethodOrderer))]

namespace Orderer.Tests;

// 4.0: method sırası artık ayrı bir orderer'ın işi
public sealed class AlphabeticalMethodOrderer : ITestMethodOrderer
{
    public IReadOnlyCollection<TTestMethod?> OrderTestMethods<TTestMethod>(IReadOnlyCollection<TTestMethod?> testMethods)
        where TTestMethod : notnull, ITestMethod =>
        testMethods.OrderBy(m => m?.MethodName, StringComparer.Ordinal).ToArray();
}

İlk denememde dönüş ve parametre tipini TTestMethod? yerine TTestMethod yazdım ve derleyici CS8614 ile interface'in nullable imzasını hatırlattı. Düzeltmeden sonra sıra yine A, B, C, D. Bu yazı, bu tür sessiz kırılmalar dahil, eski test projelerini okumayı ve v2'den v3'e, 3.x'ten 4.0'a taşımayı adım adım anlatıyor.

v2 Projesini Tanımak

Bir test projesinin v2 olduğunu .csproj'dan anlarsın. Sitedeki ASP.NET Core Web API (Application Programming Interface) serisinin 29. yazısı xunit 2.9.3, xunit.runner.visualstudio 3.0.2 ve Microsoft.NET.Test.Sdk 17.13.0 kullanıyor; C# İleri serisinin 14. yazısı xunit 2.9.3, xunit.runner.visualstudio 2.8.2 ve Microsoft.NET.Test.Sdk 17.12.0. İkisi de v2: library proje, VSTest adapter'ı ve Xunit.Abstractions. Bu yazılar yazıldıklarında doğru tercihti ve v2 hâlâ çalışıyor; ama yeni özelliklerin hepsi v3'te.

Dikkat çekici bir ayrıntı: .NET 10 SDK'sının (Software Development Kit, burada 10.0.402) kendi dotnet new xunit template'i hâlâ v2 üretiyor. Denediğimde oluşan projede xunit 2.9.3, xunit.runner.visualstudio 3.1.4, Microsoft.NET.Test.Sdk 17.14.1 ve coverlet.collector 6.0.4 vardı. v3 için 02. yazıdaki xunit.v3.templates paketinin xunit3 template'i gerekiyor. Yeni bir projeye "dotnet new xunit" ile başlayan biri, farkında olmadan v2 ile başlıyor.

Adım Adım: Paketler, Proje ve global.json

Taşıma örneği, v2 döneminden tipik bir test dosyası: Xunit.Abstractions'tan ITestOutputHelper, Task dönen bir IAsyncLifetime fixture'ı, IEnumerable<object[]> ile MemberData, assembly seviyesinde paralelliği kapatan bir CollectionBehavior ve 2019'dan kalma bir async void test. v2'de beş test, dördü geçiyor, biri atlanıyor; tek uyarı xUnit1048: "Support for 'async void' unit tests is being removed from xUnit.net v3."

İlk adım proje dosyası. Önce ve sonra:

<!-- v2 -->
<ItemGroup>
  <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.11.0" />
  <PackageReference Include="xunit" Version="2.9.3" />
  <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>

<!-- v3 / 4.0 -->
<PropertyGroup>
  <OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
  <PackageReference Include="xunit.v3.mtp-v2" Version="4.0.2" />
</ItemGroup>

Üç paket tek pakete iniyor, proje executable oluyor ve çözümün global.json'ına 02. yazıdaki "test": { "runner": "Microsoft.Testing.Platform" } bölümü ekleniyor. Ekip VSTest'te kalmak istiyorsa xunit.v3.mtp-off + xunit.runner.visualstudio 4.x + Microsoft.NET.Test.Sdk kombinasyonu var; ama 4.0 ile MTP (Microsoft Testing Platform) v2 varsayılan ve yeni geliştirmeler orada.

Derleme Hataları ve Çözümleri

Paketleri değiştirip derlediğimde aldığım hatalar (yollar kısaltıldı):

CatalogTests.cs(2,13): error CS0234: The type or namespace name 'Abstractions' does not exist in the namespace 'Xunit' (are you missing an assembly reference?)
CatalogTests.cs(4,31): error CS0619: 'CollectionBehaviorAttribute.DisableTestParallelization' is obsolete: 'Please set ParallelizationAttribute.Mode instead. This property will be removed in the next major version.'
CatalogTests.cs(8,40): error CS0738: 'PriceListFixture' does not implement interface member 'IAsyncDisposable.DisposeAsync()'. ...
CatalogTests.cs(8,40): error CS0738: 'PriceListFixture' does not implement interface member 'IAsyncLifetime.InitializeAsync()'. ...

Bunlar düzeltildikten sonraki ikinci tur:

CatalogTests.cs(46,23): error xUnit1049: Support for 'async void' unit tests has been removed from xUnit.net v3. Convert the test to 'async Task' or 'async ValueTask' instead.
CatalogTests.cs(48,15): warning xUnit1051: Calls to methods which accept CancellationToken should use TestContext.Current.CancellationToken to allow test cancellation to be more responsive.

Taşınmış dosya:

using Xunit;
using Xunit.Sdk;
using Xunit.v3;

[assembly: Parallelization(Mode = ParallelMode.None)]

namespace Bookstore.Legacy.Tests;

public sealed class PriceListFixture : IAsyncLifetime
{
    public Dictionary<string, decimal> Prices { get; } = [];

    public ValueTask InitializeAsync()
    {
        Prices["978-0321125217"] = 55m;
        return ValueTask.CompletedTask;
    }

    public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}

public sealed class CatalogTests(PriceListFixture prices, ITestOutputHelper output) : IClassFixture<PriceListFixture>
{
    public static TheoryData<decimal, decimal> Discounts =>
    [
        (100m, 10m),
        (99.99m, 0m),
    ];

    [Fact]
    public void Known_book_has_a_price()
    {
        output.WriteLine($"Prices loaded: {prices.Prices.Count}");
        Assert.Equal(55m, prices.Prices["978-0321125217"]);
    }

    [Theory]
    [MemberData(nameof(Discounts))]
    public void Volume_discount(decimal subtotal, decimal expected) =>
        Assert.Equal(expected, subtotal >= 100m ? subtotal * 0.10m : 0m);

    [Fact(Skip = "Waiting for the new tax rules")]
    public void Vat_is_added() { }

    [Fact]
    public async Task Async_test_from_2019()
    {
        await Task.Delay(10, TestContext.Current.CancellationToken);
        Assert.True(true);
    }
}

Değişiklikleri tek tek sayalım. ITestOutputHelper artık Xunit namespace'inde; using Xunit.Abstractions; silinir. IAsyncLifetime'ın method'ları ValueTask döner ve interface IAsyncDisposable'dan türer. CollectionBehavior(DisableTestParallelization = true) 4.0'da obsolete ve hata seviyesinde; yerine Parallelization(Mode = ParallelMode.None), 05. yazıda gördüğümüz iki namespace'le. async void async Task oluyor. IEnumerable<object[]> v3'te hâlâ çalışıyor, ama bu fırsatla tip güvenli TheoryData'ya geçtim. dotnet test çıktısı: 5 test, 4 geçti, 1 atlandı.

v3'ün Getirdikleri, Kısaca

Taşıma bittiğinde kullanılabilir hâle gelen ve bu seride gördüğümüz özellikler: her test için TestContext.Current (iptal token'ı, ekler, uyarılar, key-value storage; 02 ve 05. yazılar), çalışma anında Assert.Skip, SkipUnless/SkipWhen ve Explicit testler (04), satır bazında metadata taşıyan TheoryDataRow ve kombinasyonlar için MatrixTheoryData (04), AssemblyFixture (05). Seride kullanmadığımız iki özellik daha var: xunit.runner.json'da ya da komut satırında culture ile testlerin kültürünü sabitlemek ve [assembly: TestPipelineStartup(typeof(...))] ile keşif ve çalıştırmanın başında ve sonunda çalışan ITestPipelineStartup; örneğin bir OpenTelemetry tracer'ını test koşusu boyunca açık tutmak için.

4.0: MTP v1'in Sonu, Native AOT, Paralellik ve Konsol Aracı

14 Ağustos 2026'da çıkan 4.0, 3.x'ten gelen projeler için de bir taşıma adımı. Öne çıkanlar:

DeğişiklikEtkisi
MTP v1 desteği bitti, MTP v2 varsayılanMTP v1 ile çalışan 3.x projeleri 4.0'a yükseltildiğinde MTP v2'ye geçer; xUnit'in MTP rapor seçenekleri --report-xunit- önekini aldı
Native AOT (Ahead-of-Time)xunit.v3.core.aot paketleri; template'te --native-aot seçeneği (net9.0 ve net10.0)
Tam paralellik (ParallelMode.All)Varsayılan değişmedi (Collections); eski CollectionBehavior paralellik ayarları obsolete
Yeni orderer katmanlarıCollection, class, method ve case orderer'ları; mevcut ITestCaseOrderer'lar method sıralamaz
xunit-console-toolKonsol runner'ı bir .NET tool olarak; .NET 10 SDK gerektirir
Mono desteği bittiMono'ya özel sorunlar artık kabul edilmiyor
Extensibility API'leriBirçok 3.x obsolete API kaldırıldı; Xunit.Runners yerine Xunit.SimpleRunner

Konsol aracını sürüm notu global tool olarak kurmayı gösteriyor; ben makineye global bir şey kurmamak için çalışma klasörüne local tool manifest'iyle kurdum (dotnet new tool-manifest, ardından dotnet tool install xunit-console-tool --version 4.0.2). Taşınmış projeyi derlenmiş DLL'i (Dynamic Link Library) üzerinden çalıştırınca:

xUnit.net v3 Console Runner v4.0.2+0c7aa4991f [osx-arm64]
  Discovering: Bookstore.Legacy.Tests
  Discovered:  Bookstore.Legacy.Tests
  Starting:    Bookstore.Legacy.Tests
    Bookstore.Legacy.Tests.CatalogTests.Vat_is_added [SKIP]
      Waiting for the new tax rules
  Finished:    Bookstore.Legacy.Tests (ID = 'b571046623dd405aeffede3699e8cad800434520a800752aecec6ccec7cac447')
=== TEST EXECUTION SUMMARY ===
   Bookstore.Legacy.Tests  Total: 5, Errors: 0, Failed: 0, Skipped: 1, Not Run: 0, Time: 0.080s

Konsol runner'ı birden çok assembly'yi tek komutla çalıştırmak ya da dotnet test'in olmadığı bir ortamda (yalnız derlenmiş çıktının dağıtıldığı bir test ajanı gibi) testleri koşturmak için kullanışlı.

Taşıma Checklist'i

  • Paketler: xunit, xunit.runner.visualstudio 2.x/3.x ve Microsoft.NET.Test.Sdk'yı kaldır, xunit.v3.mtp-v2 (ya da VSTest için xunit.v3.mtp-off + adapter 4.x) ekle; OutputType'ı Exe yap; sürümü Directory.Packages.props'ta merkezîleştir.
  • Runner: çözümün global.json'ına MTP runner'ını ekle; bütün test projelerini aynı anda taşı, VSTest ve MTP'yi karıştırma.
  • Namespace ve imzalar: Xunit.Abstractions'ı sil; IAsyncLifetime'ı ValueTask'a çevir; async void testleri async Task yap; analyzer'ın xUnit1051 uyarıları için TestContext.Current.CancellationToken geçir.
  • Paralellik: CollectionBehavior'ın paralellik property'lerini Parallelization attribute'una ya da xunit.runner.json'daki parallelMode'a taşı; All'a geçmeden önce paylaşılan state'i ara.
  • Sıralama ve extensibility: özel orderer, discoverer ya da attribute varsa 4.0 sürüm notlarını satır satır oku ve davranışı bir testle doğrula; derlenmesi çalıştığı anlamına gelmiyor.
  • Bağımlı paketler: Verify, AutoFixture, FsCheck gibi xUnit entegrasyon paketlerinin v3 karşılıklarını (Verify.XunitV3, AutoFixture.Xunit3, FsCheck.Xunit.v3) ve sürüm sınırlarını kontrol et.
  • CI (Continuous Integration): test adımını dotnet test'in MTP moduna göre güncelle (--project/--solution, --report-xunit-trx), test sayısının taşımadan önceyle aynı olduğunu doğrula.

Üretimde Dikkat

Taşımadan önce ve sonra test sayısını karşılaştır. Bir theory'nin satırlarının farklı keşfedilmesi, bir attribute'un sessizce görmezden gelinmesi ya da bir filtrenin farklı yorumlanması, testlerin "geçmesine" ama daha azının çalışmasına yol açabilir. --minimum-expected-tests ile CI'da bir alt sınır koymak bu riski kapatır; 02. yazıdaki exit code 8 de aynı amaca hizmet ediyor.

Büyük çözümleri proje proje değil, aşamalı ama koordineli taşı. Önce bütün projelerde paket ve namespace değişikliklerini yap (VSTest modunda, -mtp-off paketleriyle bile olabilir), sonra tek bir commit'le runner'ı MTP'ye çevir. Bu, "bir çözümde tek runner" kuralını taşıma boyunca korur.

Taşıma bir refactoring fırsatı ama refactoring değil. IEnumerable<object[]>'yı TheoryData'ya çevirmek gibi küçük iyileştirmeler makul; test mantığını aynı commit'te değiştirmek ise kırmızı bir testin taşımadan mı yoksa değişiklikten mi geldiğini ayırt etmeyi zorlaştırır.

Sık Düşülen Tuzaklar

Custom ITestCaseOrderer'ların 4.0'da kırılması. Önce Tahmin Et'in kendisi: derleme geçiyor, testler geçiyor, sıra kayboluyor. Sıraya güvenen bir test grubu varsa, taşımadan sonra sırayı doğrulayan bir test yaz ya da daha iyisi, sıra bağımlılığını kaldır.

Eski paralellik ayarlarını taşımak. CollectionBehavior(MaxParallelThreads = 4) gibi ayarlar 4.0'da obsolete; DisableTestParallelization örneğimizde hata seviyesindeydi. Ayarı yeni attribute'a taşırken, eski ayarın neden konduğunu da sorgula: çoğu zaman paylaşılan bir state'i gizlemek içindir.

dotnet new xunit'e güvenmek. Yukarıda gördüğümüz gibi SDK'nın yerleşik template'i v2 üretiyor. Yeni projede xunit3 template'ini kullan ve template'in net8.0 varsayılanını unutma.

Uyarıları susturarak taşımak. xUnit1051 gibi uyarıları NoWarn'a eklemek taşımayı hızlandırır, ama v3'ün en faydalı kazanımlarından birini, iptal edilebilir testleri, boşa harcar.

Küçük Bir Deneme

★☆☆ Kendi projende bir test projesinin .csproj'una bak: v2 mi, v3 mü, hangi runner? dotnet test --list-tests çıktısındaki test sayısını not et.

★★☆ Önce Tahmin Et'teki AlphabeticalOrderer'ı 4.0'da silmeden, bir theory'nin satırlarını ters sıraya dizen bir test case orderer'a dönüştür ve AlphabeticalMethodOrderer ile birlikte kullan. Method'lar alfabetik, satırlar ters sırada mı çalışıyor?

★★★ ASP.NET Core Web API serisinin 29. yazısındaki test projesini bu yazıdaki checklist'le v3 4.0'a taşı. Kaç derleme hatası aldın, hangisi checklist'te yoktu? Taşımadan önceki ve sonraki test sayıları aynı mı?

Bu yazıyla Unit Test serisi tamamlanıyor. Başladığımız soru, "unit" nedir, şimdi daha somut bir cevaba sahip: davranışı test eden, refactoring'e dayanan, hızlı ve okunur testler; mock'lar yalnız sistem sınırında; tasarım testin işini kolaylaştıracak şekilde. Sıradaki adım kırmızı-yeşil-refactor döngüsü: testi koddan önce yazmak TDD (Test-Driven Development) serisinin konusu; gerçek veritabanı ve HTTP (HyperText Transfer Protocol) ile çalışan testler ise Integration ve E2E (end-to-end) Test serisinde.