NUnit 5 Rehberi: xUnit Bilen İçin Constraint Model, TestCase/TestCaseSource, SetUp/OneTimeSetUp, Paralellik ve 5.0 Breaking Change'leri
NUnit 5.0'da [Platform("NET")] artık .NET Framework değil modern .NET demek: NUnit 4'te .NET 10'da hep atlanan eski bir test, yükseltmeyle ilk kez çalıştı ve production'daki gerçek bir encoding hatasını gösterdi. xUnit karşılık tablosu, paylaşılan fixture instance'ı, Assert.That constraint'leri, TestCase/TestCaseSource, SetUpFixture, paralellik, MTP ve 4.6.1'den 5.0'a gerçek derleme hatalarıyla yükseltme.
Sürüm notu: Bu yazıdaki kodlar .NET 10, NUnit 5.0.0 (karşılaştırma için 4.6.1), NUnit3TestAdapter 6.3.1 ve NUnit.Analyzers 4.15.0 ile derlenip denenmiştir.
Önce Tahmin Et
Seri boyunca xUnit kullandık, ama kurumsal .NET projelerinin önemli bir kısmında testler NUnit ile yazılmış. Bu yazı NUnit'i xUnit bilen biri için anlatıyor ve bunu gerçek bir yükseltme üzerinden yapıyor. Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) Inventory module'ünde eski bir NUnit test projesi var. Projede, depo stoklarını gece eski muhasebe sistemine gönderen bir export'un testi duruyor:
[TestFixture]
public class StockExportTests
{
[Test, Platform("Net")]
public void Legacy_export_is_written_in_the_turkish_code_page()
{
byte[] file = StockCsvExport.ToLegacyCsv([new StockItem("9780441172719", "Çatalca", 12, 2)]);
string expected = "isbn;warehouse;available\n9780441172719;Çatalca;10\n";
Assert.That(file, Has.Length.EqualTo(expected.Length));
Assert.That(file, Has.Member((byte)0xC7));
}
}
Muhasebe sistemi dosyayı Windows-1254 (Türkçe kod sayfası) ile okuyor; 0xC7 bu kod sayfasında Ç. Test, proje .NET Framework'teyken yazılmış. NUnit 4.6.1 ile .NET 10'da çalışan suite'in özeti:
skipped Legacy_export_is_written_in_the_turkish_code_page
Only supported on Net
Test run summary: Passed!
total: 5
failed: 0
succeeded: 4
skipped: 1
Paket NUnit 5.0.0'a yükseltildi, derleme hataları (aşağıda göreceğiz) düzeltildi, test koduna başka dokunulmadı. Yıllardır skipped görünen bu test şimdi ne yapacak?
Cevap: "NET" Artık .NET Framework Demek Değil
Çalışıyor ve kalıyor:
failed Legacy_export_is_written_in_the_turkish_code_page (3ms)
System.NotSupportedException : No data is available for encoding 1254. For information on defining a custom encoding, see the documentation for the Encoding.RegisterProvider method.
1) at System.Text.Encoding.GetEncoding(Int32 codepage)
NUnit 4'te [Platform("Net")] .NET Framework demekti; test .NET 10'da hiç çalışmadı ve "Only supported on Net" diye atlandı. NUnit 5.0 bu adı Microsoft'un bugünkü kullanımına uydurdu: "NET" ve "DotNET" artık modern .NET'i (.NET 5 ve sonrası) gösteriyor, .NET Framework için yeni "NETFramework" ve "DotNETFramework" adları geldi. Yükseltmeyle birlikte test ilk kez .NET 10'da koştu.
Kalmasının sebebi testte değil, production kodunda: .NET (Core ve sonrası) varsayılan olarak yalnız birkaç encoding içeriyor; Windows kod sayfaları CodePagesEncodingProvider'dan geliyor. Export, .NET 10'a taşındığından beri her gece bu exception'la duruyor olmalıydı; atlanan test bunu gizledi. Düzeltme production'da:
// .NET (Core ve sonrası) yalnız birkaç encoding içeriyor; kod sayfaları bu provider'dan geliyor
Encoding turkish = CodePagesEncodingProvider.Instance.GetEncoding(1254)!;
Testten Platform attribute'u kalktı ve test yeşile döndü. Ders NUnit'e özel değil: skipped sayısı da bir metrik. CI raporunda sürekli atlanan bir test görürsen nedenini bil; ya gerçekten başka bir platform içindir ya da kimsenin bakmadığı bir hatayı saklıyordur.
xUnit Bilen İçin: Eşleme ve Paylaşılan Fixture Instance'ı
| Konu | xUnit v3 4.0 | NUnit 5.0 |
|---|---|---|
| Test class'ı | attribute gerekmez | [TestFixture] (isteğe bağlı) |
| Test | [Fact] | [Test] |
| Satır içi veri | [Theory] + [InlineData] | [TestCase] |
| Method'dan veri | [MemberData], TheoryData | [TestCaseSource], TestCaseData |
| Kombinasyon | MatrixTheoryData | [Values], [Range], [Combinatorial] |
| Her testten önce | constructor | [SetUp] |
| Her testten sonra | Dispose/DisposeAsync | [TearDown] |
| Class başına bir kez | IClassFixture<T> | [OneTimeSetUp]/[OneTimeTearDown] |
| Assembly/namespace başına | AssemblyFixture | [SetUpFixture] |
| Atlamak | Skip = "...", Assert.Skip | [Ignore("...")], Assert.Ignore |
| Test başına instance | her test için yeni | varsayılan: fixture başına bir |
| Paralellik | varsayılan açık | varsayılan kapalı, [Parallelizable] |
Tablodaki en önemli satır sondan ikincisi. xUnit her test için test class'ının yeni bir instance'ını oluşturuyor; NUnit ise varsayılan olarak bir fixture'daki bütün testler için tek instance kullanıyor. xUnit alışkanlığıyla yazılmış bir NUnit testi:
[TestFixture]
public class SharedLedgerTests
{
private const string Dune = "9780441172719";
// Field initializer: NUnit'te bütün fixture için bir kez çalışır
private readonly InMemoryStockLedger ledger = new InMemoryStockLedger().Receive(Dune, 10);
[Test]
public async Task Reserving_four_leaves_six()
{
await ledger.ReserveAsync(Dune, 4, TestContext.CurrentContext.CancellationToken);
Assert.That(ledger.Available(Dune), Is.EqualTo(6));
}
[Test]
public async Task Reserving_eight_leaves_two()
{
await ledger.ReserveAsync(Dune, 8, TestContext.CurrentContext.CancellationToken);
Assert.That(ledger.Available(Dune), Is.EqualTo(2));
}
}
failed Reserving_four_leaves_six (3ms)
Bookstore.Inventory.InsufficientStockException : Not enough stock for 9780441172719: requested 4, available 2.
Reserving_eight_leaves_two önce çalıştı (NUnit sırayı garanti etmiyor; bu koşuda testler ada göre sıralandı), stoğu 2'ye indirdi ve ikinci test o stokla başladı. xUnit'te iki test de yeşildi. İki düzeltme var: state'i [SetUp] method'unda kurmak (her testten önce çalışır) ya da fixture'a [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] koymak. İkincisiyle bu class hiç değişmeden iki test de yeşile döndü; attribute assembly seviyesinde de verilebiliyor. Mevcut bir NUnit projesinde ilk bakılacak yer field'lar: [SetUp] dışında atanan ve testlerde değişen her field bir sızıntı adayı.
Assert.That ve Constraint'ler
NUnit'in assertion'ları tek bir method üzerinden yazılıyor: Assert.That(actual, constraint). Constraint'ler Is, Has, Does, Throws class'larından zincirleniyor. Inventory'deki yeniden sipariş kuralı (ReorderPolicy, stoğun kaç gün yeteceğine bakıyor) üzerinden:
[Test]
public void Days_of_cover_is_compared_with_a_tolerance() =>
Assert.That(ReorderPolicy.DaysOfCover(10, 3), Is.EqualTo(3.33).Within(0.01));
[Test]
public void Negative_sales_are_rejected_with_the_parameter_name() =>
Assert.That(
() => ReorderPolicy.DaysOfCover(10, -1),
Throws.TypeOf<ArgumentOutOfRangeException>().ParamName.EqualTo("averageDailySales"));
[Test]
public void Only_titles_below_the_minimum_are_listed()
{
ReorderPolicy policy = new(minimumDaysOfCover: 7);
StockItem[] items = [new(Dune, "Istanbul", 3, 1), new(Hobbit, "Istanbul", 40, 0), new(Emma, "Ankara", 5, 0)];
Dictionary<string, double> sales = new() { [Dune] = 1, [Hobbit] = 2, [Emma] = 0.5 };
IReadOnlyList<string> titles = policy.TitlesToReorder(items, sales);
using (Assert.EnterMultipleScope())
{
Assert.That(titles, Has.Exactly(1).EqualTo(Dune));
Assert.That(titles, Does.Not.Contain(Hobbit));
Assert.That(titles, Has.None.EqualTo(Emma));
}
}
Within(0.01) double karşılaştırmasına tolerans ekliyor; Within(5).Percent ve tarihler için Within(TimeSpan) de var. Throws.TypeOf<T>() tam tipi, Throws.InstanceOf<T>() alt tipleri de kabul ediyor. .ParamName 5.0 ile geldi: ArgumentException ve türevleri için parametre adını doğrudan kontrol ediyor. C# 14'ün extension property özelliğiyle yazıldığı için test projesinin C# 14 kullanması gerekiyor (.NET 10'da varsayılan). Eskiden aynı iş .With.Property("ParamName") ile, derleyicinin kontrol etmediği bir string'le yapılıyordu.
Assert.EnterMultipleScope() (eski yazımı Assert.Multiple(() => ...)) blok içindeki bütün assertion'ları çalıştırıp hataları tek raporda topluyor, 15. yazıdaki AssertionScope'un karşılığı. Bilerek yanlış beklentilerle:
Multiple failures or warnings in test:
1) Assert.That(titles, Has.Exactly(2).Items)
Expected: exactly 2 items
But was: 1 matching item < "9780441172719" >
2) Assert.That(titles, Does.Contain("9780141439587"))
Expected: some item equal to "9780141439587"
But was: < "9780441172719" >
3) Assert.That(ReorderPolicy.DaysOfCover(10, 3), Is.EqualTo(3.3).Within(0.01))
Expected: 3.2999999999999998d +/- 0.01d
But was: 3.3333333333333335d
Off by: -0.033333333333333659d
Mesajın ilk satırı assertion'ın kaynak kodunu gösteriyor (NUnit 4'ten beri CallerArgumentExpression ile). Classic model (Assert.AreEqual(expected, actual), Assert.IsTrue) NUnit 4.0'da ClassicAssert adıyla NUnit.Framework.Legacy namespace'ine taşınmıştı; 4.5 bu method'ları C# 14'ün static class'lar için extension member desteğiyle Assert üzerine geri getirdi. Yeni kodda constraint model daha okunur mesaj veriyor; NUnit.Analyzers da ClassicAssert kullanımında NUnit2005 uyarısıyla bunu öneriyor.
TestCase, TestCaseSource, Values ve Combinatorial
[TestCase], xUnit'in [InlineData]'sının karşılığı; farkı, beklenen değeri de alabilmesi. Test method'u değer döndürüyor, NUnit onu ExpectedResult ile karşılaştırıyor:
[TestCase(30, 2.0, ExpectedResult = 15.0)]
[TestCase(10, 4.0, ExpectedResult = 2.5)]
[TestCase(0, 3.0, ExpectedResult = 0.0)]
public double Days_of_cover_is_available_divided_by_daily_sales(int available, double dailySales) =>
ReorderPolicy.DaysOfCover(available, dailySales);
Verinin hesaplanması ya da her satıra ad verilmesi gerekiyorsa [TestCaseSource]. NUnit 5.0'da TestCaseData.Create(...) argüman tiplerini çıkarıyor ve .Returns(...) beklenen sonucun tipini de kontrol ediyor; 4.x'te Returns object alıyordu ve yanlış tipteki bir sonuç ancak çalışma anında fark ediliyordu:
private static IEnumerable<TestCaseDataWithReturn<int, int, bool>> WarehouseSplits()
{
yield return TestCaseData.Create(10, 3).SetName("13 copies in two warehouses").Returns(true);
yield return TestCaseData.Create(10, 4).SetName("14 copies in two warehouses").Returns(false);
}
[TestCaseSource(nameof(WarehouseSplits))]
public bool Stock_is_summed_across_warehouses(int istanbul, int ankara)
{
ReorderPolicy policy = new(minimumDaysOfCover: 7);
StockItem[] items = [new(Dune, "Istanbul", istanbul, 0), new(Dune, "Ankara", ankara, 0)];
return policy.TitlesToReorder(items, new Dictionary<string, double> { [Dune] = 2 }).Contains(Dune);
}
Günde 2 satış ve 7 günlük minimum, 14 adet demek; iki depodaki 13 adet yetmiyor, 14 adet yetiyor. [Values] ve [Range] parametre başına değer listesi veriyor, [Combinatorial] (varsayılan) bütün kombinasyonları üretiyor:
[Test, Combinatorial]
public void Days_of_cover_is_never_negative(
[Values(0, 1, 100)] int available, [Range(0.5, 2.0, 0.5)] double dailySales) =>
Assert.That(ReorderPolicy.DaysOfCover(available, dailySales), Is.GreaterThanOrEqualTo(0));
3 × 4 = 12 test case. [Sequential] değerleri sırayla eşliyor, [Pairwise] her değer çiftini en az bir kez kapsayan daha küçük bir küme seçiyor. Hangi değerlerin seçileceği (boundary value'lar, equivalence class'lar) ilerideki Test Tasarım Teknikleri serisinin konusu.
SetUp, OneTimeSetUp ve SetUpFixture
[SetUp] ve [TearDown] her testin önünde ve arkasında çalışıyor. [OneTimeSetUp] ve [OneTimeTearDown] fixture başına bir kez; fixture instance'ı paylaşıldığı için bunlar instance method'u olabiliyor (xUnit'teki IClassFixture ayrı bir class istiyordu). [SetUpFixture] ise bir namespace'in bütün fixture'larından önce ve sonra bir kez çalışıyor:
// Bu namespace'teki bütün fixture'lardan önce bir kez çalışır; yalnız salt okunur veri tutar
[SetUpFixture]
public class InventoryTestRun
{
public static IReadOnlyList<StockItem> LargeWarehouse { get; private set; } = [];
[OneTimeSetUp]
public void BuildLargeWarehouse() =>
LargeWarehouse =
[
.. Enumerable.Range(0, 10_000)
.Select(i => new StockItem($"978{i:D10}", i % 2 == 0 ? "Istanbul" : "Ankara", OnHand: i % 50, Reserved: 0)),
];
}
10.000 kalemlik depo bir kez kuruluyor ve ReorderPolicyTests içindeki bir test onu okuyor (stoğu 7'nin altında kalan 1.400 başlık). Burada paylaşılan veri değiştirilemez bir liste; paylaşılan bir InMemoryStockLedger olsaydı yukarıdaki sızıntının daha büyüğünü yaşardık. NUnit 5.0, [OneTimeSetUp] içinde TestContext.CurrentContext.ActiveTests ile filtreden geçen testleri görmeyi de ekledi: pahalı bir kurulumu, onu isteyen test çalışmayacaksa atlamak için.
4.x'ten 5.0'a: Derleme Hataları ve Async Assertion'lar
NUnit 5.0.0 27 Eylül 2026'da çıktı; lisansı MIT (kısıtlaması çok az, kısa bir açık kaynak lisansı). NUnit dokümanındaki "NUnit 5 - A summary of the breaking changes" sayfası değişiklikleri tek tek gerekçesiyle listeliyor; yükseltmeden önce okunacak sayfa o. Önce Tahmin Et'teki projenin diğer test dosyası 4.6.1'de şöyleydi:
using Bookstore.Inventory;
using NUnit.Framework.Legacy;
using CollectionAssert = NUnit.Framework.Legacy.CollectionAssert;
namespace Bookstore.Inventory.Tests;
[TestFixture]
public class InMemoryStockLedgerTests
{
private const string Dune = "9780441172719";
private InMemoryStockLedger ledger = null!;
[SetUp]
public void SetUp() => ledger = new InMemoryStockLedger().Receive(Dune, 10);
[Test]
public void Reserving_more_than_available_is_rejected()
{
Assert.ThrowsAsync<InsufficientStockException>(() => ledger.ReserveAsync(Dune, 25, CancellationToken.None));
}
[Test]
public void Rejection_reports_the_available_quantity()
{
InsufficientStockException? ex = Assert.ThrowsAsync<InsufficientStockException>(
async () => await ledger.ReserveAsync(Dune, 25, CancellationToken.None));
ClassicAssert.AreEqual(10, ex?.Available);
}
[Test]
public void Negative_daily_sales_are_rejected()
{
TestDelegate daysOfCover = () => ReorderPolicy.DaysOfCover(10, -1);
Assert.Throws<ArgumentOutOfRangeException>(daysOfCover);
}
[Test]
public void Reservation_reduces_available_stock()
{
ledger.ReserveAsync(Dune, 4, CancellationToken.None).GetAwaiter().GetResult();
ClassicAssert.AreEqual(6, ledger.Available(Dune));
CollectionAssert.AreEqual(new[] { 6 }, new[] { ledger.Available(Dune) });
}
}
4.6.1 iki uyarı veriyordu; NUnit yükseltmeyi önceden haber veriyor:
InMemoryStockLedgerTests.cs(34,9): warning CS0618: 'TestDelegate' is obsolete: 'Use Action instead of TestDelegate'
InMemoryStockLedgerTests.cs(35,9): warning CS0618: 'Assert.Throws<TExpected>(TestDelegate)' is obsolete: 'Use overload with Action instead of TestDelegate'
Directory.Packages.props'ta sürümü 5.0.0 yapınca derleme üç hatayla durdu (ilki tek başına geldi, düzeltince diğer ikisi göründü):
InMemoryStockLedgerTests.cs(3,49): error CS0234: The type or namespace name 'CollectionAssert' does not exist in the namespace 'NUnit.Framework.Legacy' (are you missing an assembly reference?)
InMemoryStockLedgerTests.cs(24,42): error CS0029: Cannot implicitly convert type 'System.Threading.Tasks.Task<Bookstore.Inventory.InsufficientStockException?>' to 'Bookstore.Inventory.InsufficientStockException'
InMemoryStockLedgerTests.cs(33,9): error CS0246: The type or namespace name 'TestDelegate' could not be found (are you missing a using directive or an assembly reference?)
Üçü de 5.0'ın bilinçli değişiklikleri:
<ul>
<li><code>StringAssert</code>, <code>CollectionAssert</code>, <code>FileAssert</code> ve <code>DirectoryAssert</code> 4.0'da taşındıkları <code>NUnit.Framework.Legacy</code>'den tekrar <code>NUnit.Framework</code>'e döndü. 4.0 geçişinde eklenen alias'lar kırılıyor; alias'ı silmek yetti. <code>ClassicAssert</code> hâlâ <code>Legacy</code>'de.</li>
<li><code>Assert.ThrowsAsync</code>, <code>CatchAsync</code> ve <code>DoesNotThrowAsync</code> artık gerçekten asenkron ve <code>Task</code> döndürüyor; await edilmeleri gerekiyor. Sonucu bir değişkene atanan çağrı derlenmiyor, test method'u <code>async Task</code> oluyor.</li>
<li><code>TestDelegate</code>, <code>AsyncTestDelegate</code> ve <code>ActualValueDelegate<T></code> kaldırıldı; yerlerine <code>Action</code>, <code>Func<Task></code> ve <code>Func<T></code>. Çok sayıda kullanım varsa dokümandaki öneri <code>global using TestDelegate = System.Action;</code> alias'ı.</li>
<li>Derleme hatası vermeyen ama davranışı değiştirenler: <code>[Platform("NET")]</code> (Önce Tahmin Et), değer tiplerinde <code>Is.SameAs</code>'in artık derlenmemesi, <code>[Order]</code>'ın obsolete olup yerini <code>[DependsOnTest]</code>'e bırakması ve .NET 6 hedefinin düşmesi (5.0 <code>net462</code>, <code>net8.0</code> ve <code>net10.0</code> için derleniyor).</li>
</ul>
En tehlikelisi ilk satırdaki test: Assert.ThrowsAsync'in sonucu kullanılmıyor, bu yüzden derleme hatası yok. Dokümana göre await edilmeyen async assertion değerlendirilmiyor. Bizim projede bu test yine de doğru çalıştı: ReserveAsync'i 25 adet yerine yetecek bir miktara izin verecek şekilde bozunca kırmızıya döndü, çünkü InMemoryStockLedger senkron tamamlanıyor ve assertion, test method'u bitmeden çalışıp hatayı sonuca yazıyor. Kod gerçekten asenkron olunca (örneğin araya bir await Task.Yield() girince, gerçek bir veritabanı ya da HTTP çağrısında olduğu gibi) aynı yazım, yanlış davranan koda rağmen yeşil geçti. Yani testin güvenilirliği, test edilen kodun içinde bir await'in gerçekten beklemesine bağlı.
Bunu derleme anında yakalamanın yolu NUnit.Analyzers. 4.15.0 sürümünü (12 Eylül 2026) ekleyince:
InMemoryStockLedgerTests.cs(18,9): error NUnit2059: Method 'ThrowsAsync' returns a Task and is not being observed for completion or errors (https://github.com/nunit/nunit.analyzers/tree/master/documentation/NUnit2059.md)
InMemoryStockLedgerTests.cs(27,9): warning NUnit2005: Consider using the constraint model, Assert.That(actual, Is.EqualTo(expected)), instead of the classic model, ClassicAssert.AreEqual(expected, actual) (https://github.com/nunit/nunit.analyzers/tree/master/documentation/NUnit2005.md)
NUnit2059 varsayılan olarak hata; IDE'deki code fix await'i ekleyip method'u async Task yapıyor. Yükseltmeden önce analyzer'ı eklemek, hangi testlerin etkileneceğini derlemeden önce gösteriyor.
Paralellik, TestContext ve Microsoft.Testing.Platform
NUnit'te paralellik varsayılan olarak kapalı; [Parallelizable] ile açılıyor ve kapsamı ParallelScope belirliyor: Self (yalnız bu test ya da fixture), Children (alt testler kendi aralarında), Fixtures (fixture'lar birbirleriyle), All. Projede assembly seviyesinde:
// Fixture'lar paralel çalışır; bir fixture'ın içindeki testler sırayla
[assembly: Parallelizable(ParallelScope.Fixtures)]
Bu, paylaşılan fixture instance'ıyla uyumlu bir başlangıç: aynı fixture'daki testler aynı field'ları kullandığı için sırayla çalışıyor. Testleri de paralel çalıştırmak istiyorsan (ParallelScope.All ya da Children), önce [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] ile her teste ayrı instance ver. [NonParallelizable] tek bir fixture'ı ya da testi dışarıda bırakıyor. Eşzamanlı çalışan worker sayısı [assembly: LevelOfParallelism(n)] ile ayarlanıyor.
TestContext.CurrentContext testin adı, sonucu, test dizini ve CancellationToken gibi bilgileri veriyor. Token, test ya da fixture'a [CancelAfter(ms)] konunca o süre sonunda iptal ediliyor; testlerde ReserveAsync'e CancellationToken.None yerine onu verdik. Çıktı yazmak için TestContext.Out.WriteLine, uzun bir testte anlık ilerleme için TestContext.Progress.
NUnit, Microsoft.Testing.Platform (MTP) üzerinde NUnit3TestAdapter ile çalışıyor; 6.3.1 sürümü 9 Ekim 2026'da çıktı. Serinin global.json'ı dotnet test'i MTP modunda çalıştırdığı için test projesine iki property yetti:
<PropertyGroup>
<OutputType>Exe</OutputType>
<EnableNUnitRunner>true</EnableNUnitRunner>
</PropertyGroup>
dotnet test artık solution'daki xUnit ve NUnit projelerini birlikte çalıştırıyor (124 test, hepsi yeşil). Bir fark: xUnit'in desteklediği --treenode-filter NUnit projesinde "Zero tests ran" ile bitti; NUnit, VSTest'ten gelen --filter "FullyQualifiedName~SharedLedgerTests" syntax'ını kullanıyor.
Üretimde Dikkat
Yükseltmeyi iki adımda yap. Önce 4.6.1'de kal, NUnit.Analyzers'ın son sürümünü ekle ve CS0618 ile NUnit2059 uyarılarını temizle; bu adım davranışı değiştirmiyor. Sonra 5.0.0'a geç. Derleme hatası kalmasa bile [Platform] kullanan testleri tek tek gözden geçir; sonuç tablosundaki skipped sayısının değişmesi beklenen bir şey.
Analyzer'ı güncel tut. .NET 10 SDK'nın (10.0.404) dotnet new nunit template'i NUnit.Analyzers'ı ekliyor, ama 4.7.0 sürümüyle (NUnit 4.3.2 ve NUnit3TestAdapter 5.0.0 ile birlikte); NUnit2059 için en az 4.14 gerekiyor. Template'ten oluşturulmuş eski projelerde sürümleri elle yükselt; NUnit2059 gibi kurallar, derleyicinin göremediği async hatalarını yakalayan tek şey.
Karma bir solution'da framework'leri tek bir runner modunda tut. xUnit v3, MSTest ve NUnit projeleri aynı dotnet test komutuyla MTP'de birlikte çalışıyor; bir projeyi VSTest modunda bırakmak, 02. yazıda anlattığımız mod karışıklığı hatasını getiriyor.
Sık Düşülen Tuzaklar
[OneTimeSetUp]'ta oluşan state'in testler arasında sızması. Fixture instance'ı paylaşıldığı için [OneTimeSetUp]'ta kurulan değiştirilebilir her object bütün testlerin ortak malı; sonuç testlerin çalışma sırasına bağlı oluyor. Paylaşacaksan salt okunur veri paylaş, değişen state'i [SetUp]'ta kur.
Await edilmeyen async assertion. 4.x'ten kalma Assert.ThrowsAsync(...) satırları 5.0'da derleniyor ama kodun gerçekten asenkron olduğu yerde hiçbir şey doğrulamıyor. Analyzer olmadan fark edilmiyor.
Field initializer'ı constructor sanmak. xUnit'te field initializer her test için çalışır; NUnit'te fixture başına bir kez. Yukarıdaki SharedLedgerTests bunun en kısa örneği.
Assertion'ı ters yazmak. Assert.That(actual, Is.EqualTo(expected)) sırası xUnit'in Assert.Equal(expected, actual) sırasının tersi; iki framework'ü birlikte kullanan ekiplerde hata mesajındaki "Expected" ve "But was" yer değiştiriyor.
Küçük Bir Deneme
★☆☆ SharedLedgerTests'i projene ekle, önce olduğu gibi, sonra [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] ile çalıştır. Sonra attribute'u kaldırıp ledger'ı [SetUp]'ta kur. Üç sonucu karşılaştır.
★★☆ WarehouseSplits'e üçüncü bir satır ekle ve .Returns("yes") yaz. 5.0'da ne oluyor? Aynı satırı 4.x'teki new TestCaseData(10, 5).Returns("yes") biçiminde yazınca ne oluyor?
★★★ Kendi projendeki bir NUnit 4 test projesini bu yazıdaki iki adımla 5.0'a yükselt. Kaç CS0618 ve NUnit2059 buldun, skipped sayısı değişti mi?
Bir sonraki ve serinin son yazısında MSTest 4'e bakıyoruz: MSTest.Sdk, [DataRow]/[DynamicData], Assert API'si ve 3.x'ten 4.x'e değişenler, TestContext, paralellik ve analyzer'lar; sonunda TUnit'e kısa bir bakış ve "hangi framework" sorusu.