XUnit vs NUnit
TLDR: NUnit je lepší než xUnit – má více možností setupu, lepší dokumentaci, lepší asserty a lepší testovací atributy.
Pokud vygooglíte „xUnit vs NUnit“, najdete mnoho článků, které považují xUnit za lepší testovací framework. Následující seznam shrnuje nejčastější argumenty pro použití xUnitu:
- xUnit chrání programátora před psaním chybných testů tím, že vytváří novou instanci třídy pro každý spuštěný test. 1, 2
- xUnit spouští testy paralelně na rozdíl od NUnitu, který testy spouští sériově. 3
- xUnit používá konstruktor a
Dispose
namísto atributů[SetUp]
a[TearDown]
. 4, 5
Většina těchto argumentů je buď nepřesná, nebo zastaralá. Pojďme je tedy po jednom rozebrat a uvést na pravou míru.
1. Vytváření instance pro každý test
NUnit od verze 3.13 dokáže vytvářet novou instanci třídy pro každý test stejně jako xUnit. Pro aktivaci této funkcionality stačí do testovacího projektu přidat atribut [assembly: FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
. Dokumentaci k tomuto atributu můžete najít zde.
2. Paralelní spouštění testů
NUnit i xUnit umějí spouštět testy paralelně. Pro nastavení paralelizace ale používají velice odlišný způsob, a proto se blíže podíváme na tyto rozdíly.
xUnit
Výchozí chování
- Testy uvnitř jedné třídy běží sériově.
- Testy z různých tříd se spouští paralelně.
Testy v jedné třídě neběží paralelně! To může způsobit pomalejší běh testů. Toto chování je možné změnit pomocí balíčku třetí strany.
Nastavení paralelizace
- Můžeme použít
[CollectionDefinition("collection name")]
a tím zajistit, že testy ve více třídách budou spuštěny sériově. [assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)]
- spustí všechny testy sériově.[CollectionDefinition(DisableParallelization = true)]
- spustí zvolenou kolekci (třídu) až po doběhnutí všech ostatních paralelních kolekcí.
Další nastavení jsou obtížná nebo přímo nemožná. Více informací zde.
NUnit
Výchozí chování
- Všechny testy se spouští sériově.
Nastavení paralelizace
- Atribut
[assembly: Parallelizable(ParallelScope.Children)]
umožňuje spouštět všechny testy v assembly paralelně. - Pokud chceme stejné chování jako v xUnitu (spouštět pouze kolekce paralelně), můžeme použít atribut
[assembly: Parallelizable(ParallelScope.Fixtures)]
. - Atribut
Parallelizable
je možné využít mnoha dalšími způsoby a nastavit prakticky jakoukoliv paralelizaci si přejeme. Více zde. - Máme k dispozici také atribut
NonParallelizable
, který zajistí, že daný test nikdy není spuštěn paralelně, i přesto, že jeho kolekce nebo assembly má nastaveno paralelní spuštění.
3. Konstruktor vs. SetUp
NUnit
Máme následující možnosti setupu:
[SetUp]
- Spustí se před každým testem.[TearDown]
- Spustí se po každém testu.[OneTimeSetUp]
- Spustí se jednou před spuštěním všech testů v dané třídě (přesněji v dané fixtuře/kolekci).[OneTimeTearDown]
- Spustí se jednou po spuštění všech testů v dané třídě (přesněji v dané fixtuře/kolekci).
Všechny metody označené těmito atributy mohou být asynchronní nebo synchronní.
Pokud chceme spustit jednorázový setup nebo teardown předtím, než se spustí jakékoliv testy v assembly, můžeme použít [SetUpFixture]
.
[SetUpFixture]
public class MySetUpClass
{
[OneTimeSetUp]
public void RunBeforeAnyTests()
{
// ... Kód se spustí jednou před všemi testy v assembly
}
[OneTimeTearDown]
public void RunAfterAnyTests()
{
// ... Kód se spustí jednou po všech testech v assembly
}
}
xUnit
Setup, který běží pro každý test
Máme následující možnosti setupu:
Konstruktor třídy
- spustí se před každým testem. Ekvivalent k NUnit[SetUp]
.Dispose
(implementacíIDisposable
) - spustí se po každém testu. Ekvivalent k NUnit[TearDown]
.
Pokud chceme v setupu spustit asynchronní kód, musíme implementovat interface IAsyncLifetime
:
public class MyTests : IAsyncLifetime
{
public async Task InitializeAsync()
{
// Asynchronní setup kód běžící před každým testem
}
public async Task DisposeAsync()
{
// Asynchronní teardown kód běžící po každém testu
}
}
Setup, který běží pro celou třídu (kolekci)
Pokud potřebujeme jednorázový setup pro všechny testy v jedné třídě (nebo kolekci) v xUnitu, je nutné vytvořit samostatnou třídu (fixture), implementovat v ní IDisposable
(nebo IAsyncLifetime
pro asynchronní operace) a tuto třídu pak injectovat do konstruktoru testovací třídy pomocí interface IClassFixture<TFixture>
:
//------------------- XUNIT ------------------------------
// Fixture třída pro sdílený setup/teardown
public class DatabaseFixture : IDisposable
{
public SqlConnection Db { get; private set; }
public DatabaseFixture()
{
Db = new SqlConnection("MyConnectionString");
}
public void Dispose()
{
Db.Dispose();
}
}
// Testovací třída využívající fixture
public class MyDatabaseTests : IClassFixture<DatabaseFixture>
{
private readonly DatabaseFixture _fixture;
// Fixture je injectována konstruktorem
public MyDatabaseTests(DatabaseFixture fixture)
{
_fixture = fixture;
}
[Fact]
public void Test1()
{
// Použití sdíleného připojení _fixture.Db
Assert.NotNull(_fixture.Db);
}
}
V NUnitu bychom mohli stejný kód napsat výrazně jednodušeji pomocí atributů [OneTimeSetUp]
a [OneTimeTearDown]
přímo v testovací třídě:
//------------------- NUNIT ------------------------------
public class MyDatabaseTests
{
private SqlConnection _db;
[OneTimeSetUp]
public void FixtureSetup()
{
_db = new SqlConnection("MyConnectionString");
}
[OneTimeTearDown]
public void FixtureTearDown()
{
_db?.Dispose();
}
// ... testy využívající _db ...
[Test]
public void Test1()
{
Assert.NotNull(_db);
}
}
Pokud chceme asynchronní setup/teardown v xUnitu, je situace ještě komplikovanější (fixture třída musí implementovat IAsyncLifetime
):
//------------------- XUNIT (Async Fixture) ------------------------------
// Asynchronní fixture
public class AsyncDatabaseFixture : IAsyncLifetime
{
public SqlConnection Db { get; private set; }
public async Task InitializeAsync()
{
Db = new SqlConnection("MyConnectionString");
}
public async Task DisposeAsync()
{
await Db.DisposeAsync(); // Nebo synchronní Dispose
}
}
// Testovací třída využívající asynchronní fixture
public class MyAsyncDatabaseTests : IClassFixture<AsyncDatabaseFixture>
{
private readonly AsyncDatabaseFixture _fixture;
public MyAsyncDatabaseTests(AsyncDatabaseFixture fixture)
{
_fixture = fixture;
}
// ... testy využívající _fixture.Db ...
}
Stejný asynchronní setup/teardown v NUnitu:
//------------------- NUNIT (Async Fixture) ------------------------------
public class MyAsyncDatabaseTests
{
private SqlConnection _db;
[OneTimeSetUp] // Podporuje async Task
public async Task FixtureSetupAsync()
{
// Asynchronní setup
_db = new SqlConnection("MyConnectionString");
}
[OneTimeTearDown] // Podporuje async Task
public async Task FixtureTearDownAsync()
{
await _db.DisposeAsync(); // Pokud podporuje
}
// ... testy využívající _sqlConnection ...
}
Setup pro celou assembly
Od verze xUnitu 3.0 je možné použít [AssemblyFixture]
.
[assembly: AssemblyFixture(typeof(MyAssemblyFixture))]
public class MyAssemblyFixture : IAsyncLifetime
{
public async Task InitializeAsync()
{
// Asynchronní setup pro celou assembly
}
public async Task DisposeAsync()
{
// Asynchronní teardown pro celou assembly
}
}
NUnit vs. Internetové Mýty
Jak můžete vidět, problémy zmiňované na začátku článku (instance per test, paralelismus) lze v NUnitu snadno nakonfigurovat, nebo jsou v něm řešeny flexibilněji než v xUnitu (setup/teardown).
Další nevýhody xUnitu
Dokumentace xUnitu je často popisována jako nedostatečná. Oficiální stránky xUnitu popisují několik základních scénářů, ale nikde není k dispozici žádný úplný výčet všech assertovacích metod, testovacích atributů a konfiguračních možností xUnitu. Na tyto problémy poukazuje také několik issues na GitHubu, například zde. NUnit má oproti tomu velmi dobrou, přehlednou a komplexní dokumentaci.
Téměř neexistující oficiální dokumentace xUnitu velmi ztěžuje detailní porovnání obou frameworků. I přesto se pokusím porovnat další aspekty, kterými jsou: testovací atributy a assertovací metody.
Testovací atributy
XUnit i NUnit nabízejí velké množství pokročilejších atributů, jako je [TestCase]
(NUnit) nebo [Theory]
s [InlineData]
(xUnit), které umožňují spustit jednu testovací metodu s různými parametry.
Zde je výčet všech testovacích atributů podporovaných NUnitem. xUnit bohužel žádný takový oficiální souhrnný seznam nemá a nejlepší dostupný přehled je tento porovnávací dokument, který ale není zdaleka úplný a detailní.
Podle dostupných informací a zkušeností komunity se zdá, že NUnit podporuje širší škálu vestavěných atributů pro různé scénáře (např. [Retry]
, [Repeat]
, [Timeout]
, detailnější [Values]
, [Range]
, [Random]
, [Combinatorial]
). Bez úplného seznamu pro xUnit je ale obtížné udělat definitivní závěr.
Asserty
Oficiální porovnání na stránkách xUnitu samo poukazuje na skutečnost, že NUnit obsahuje mnohem větší množství vestavěných assertů. Dokumentace xUnitu opět ale neobsahuje žádný úplný výčet všech assertovacích metod, takže nemůžeme s jistotou dojít k nějakému závěru.
NUnit má detailní dokumentaci svých assertů.
Závěr
Osobně se mi zdá, že je obtížné najít nějaký podstatný aspekt, ve kterém by byl xUnit v současnosti jednoznačně lepší než NUnit. NUnit nabízí větší flexibilitu v konfiguraci, bohatší sadu funkcí (atributy, asserty, TestContext) a výrazně lepší dokumentaci. Dřívější výhody xUnitu byly NUnitem buď implementovány, nebo překonány.