Comparing .NET Testing Frameworks.
xunit/xunit: Community-focused unit testing tool
nunit/nunit: NUnit Framework
microsoft/testfx: MSTest framework and adapter
thomhurst/TUnit: A modern, fast and flexible .NET testing framework
Comparing .NET Testing Frameworks.
xunit/xunit: Community-focused unit testing tool
nunit/nunit: NUnit Framework
microsoft/testfx: MSTest framework and adapter
thomhurst/TUnit: A modern, fast and flexible .NET testing framework

Target audience: A developer switching frameworks :)
Typically test classes and/or methods are decorated with Attributes.
| NUnit | xUnit | MSTest | TUnit | |
|---|---|---|---|---|
| Namespace | NUnit.Framework | Xunit | Microsoft.VisualStudio .TestTools.UnitTesting |
TUnit |
| Class Attribute | TestFixture (optional) | TestClass | ||
| Method Attribute | Test | Fact | TestMethod | Test |
| Ignoring | Ignore | Fact(Skip=””) | Ignore | Skip |
| Setup & Teardown | ||||
| Before all tests | OneTimeSetUp | IClassFixture<T> | ClassInitialize | Before(Class) |
| Before each test | SetUp | Constructor | TestInitialize | Before(Test) |
| After each test | TearDown | IDisposable.Dispose | TestCleanup | After(Test) |
| After all tests | OneTimeTearDown | IClassFixture<T> | ClassCleanup | After(Class) |
| Meta data | ||||
| Description | Description | Description | ||
| Categories | Category | Trait(“Category”, “”) | TestCategory | Category |
| Custom properties | Property | Trait | TestProperty | Property |
Setup in xUnit doesn’t work with Attributes!
xUnit injects the same XUnitTestsFixture to the constructor before running each [Fact].
It’s also possible to do setup/teardown for all tests within classes decorated with a CollectionAttribute with ICollectionFixture<T>, see shared-context for more info.
public class XUnitTestsFixture : IDisposable
{
public XUnitTestsFixture() { /* BeforeAllTests */ }
public void Dispose() { /* AfterAllTests */ }
}
public class XUnitTestsWithSetUp : IClassFixture<XUnitTestsFixture>
{
private readonly XUnitTestsFixture _fixture;
public XUnitTestsWithSetUp(XUnitTestsFixture fixture)
{
_fixture = fixture;
}
[Fact]
public void Test1() { }
}
As per the IClassFixture<T> example above, the XUnitTestsFixture will be injected in all test classes in the assembly.
[assembly: AssemblyFixture(typeof(XUnitTestsFixture))]
[SetUpFixture]
public class Config
{
[OneTimeSetUp]
public void SetUp() { }
[OneTimeTearDown]
public void TearDown() { }
}
[TestClass]
public class MyTestClass
{
[AssemblyInitialize]
public static void AssemblyInitialize(TestContext testContext)
{
// Or return Task!
}
[AssemblyCleanup]
public static void AssemblyCleanup()
{
// Or return Task!
}
}
public class TUnitSetup
{
[Before(Assembly)]
public static async Task AssemblySetup() { }
[After(Assembly)]
public static async Task AssemblyTeardown()
{
// Runs after the last test in GetType().Assembly
}
[AfterEvery(Assembly)]
public static async Task AllAssembliesTeardown(AssemblyHookContext context)
{
// Runs after the last test in all assemblies
// For all Before/After there exists a Before/AfterEvery
// Aside from Test/Class/Assembly there also is TestSession/TestDiscovery
}
}
bool? actual = true;
// NUnit
Assert.That(actual, Is.EqualTo(true));
// xUnit
Assert.Equal(true, actual);
// MSTest
Assert.AreEqual(true, actual);
// TUnit
await Assert.That(actual).IsEqualTo(true);
| NUnit | xUnit | MSTest | TUnit | Notes |
|---|---|---|---|---|
| Is.EqualTo | Equal | AreEqual | IsEqualTo | Using IEquatable |
| Is.Not.EqualTo | NotEqual | AreNotEqual | IsNotEqualTo | |
| Is.Null | Null | IsNull | IsNull | |
| Is.True | True | IsTrue | IsTrue | |
| Is.SameAs | Same | AreSame | IsSameReferenceAs | Same referenced object |
All but NUnit use an overload to ignore case for Start/End with.
| NUnit | xUnit | MSTest | TUnit |
|---|---|---|---|
| Is.Empty | Empty | IsEmpty | |
| Does.Contain | Contains | StringAssert.Contains | Contains |
| Does.Not.Contain | DoesNotContain | StringAssert.DoesNotMatch | DoesNotContain |
| Does.StartWith | StartsWith | StringAssert.StartsWith | StartsWith |
| Does.EndWith().IgnoreCase | EndsWith | StringAssert.EndsWith | EndsWith |
| Does.Match | Matches | StringAssert.Matches | Matches |
Action sut = () => throw new Exception("cause");
Func<Task> asyncSut = () => throw new Exception("cause");
// NUnit
var ex = Assert.Throws<Exception>(() => sut(), "failure message");
Assert.That(ex.Message, Is.EqualTo("cause"));
var ex2 = Assert.ThrowsAsync<Exception>(() => asyncSut());
Assert.That(ex2.Message, Is.EqualTo("cause"));
// xUnit
var ex = Assert.Throws<Exception>(sut);
Assert.Equal("cause", ex.Message);
var ex2 = await Assert.ThrowsAsync<Exception>(() => asyncSut());
Assert.Equal("cause", ex2.Message);
// MSTest
// DEPRECATED: ExpectedExceptionAttribute(typeof(Exception))
// DEPRECATED: ThrowsException<T>()
var ex = Assert.ThrowsExactly<Exception>(sut);
Assert.AreEqual("cause", ex.Message);
var ex2 = await Assert.ThrowsExactlyAsync<Exception>(asyncSut);
Assert.AreEqual("cause", ex2.Message);
// TUnit
// Also: .WithMessageMatching and .WithInnerException
await Assert.That(sut).ThrowsExactly<Exception>().WithMessage("cause");
await Assert.That(asyncSut)
.ThrowsExactly<ArgumentException>()
.WithMessageMatching(StringMatcher.AsRegex("^cause$"));
The above assertions expect the exceptions to be of exactly the type provided. If you do not want a failure when the actual type is a derived type:
Assert.ThrowsAny(Async)<T>Assert.Catch(Async)<T>Assert.Throws(Async)<T>var ex = Assert.Throws(Async)<T>(sut)| NUnit | xUnit | MSTest | TUnit | Notes |
|---|---|---|---|---|
| Is.Empty | Empty | Assert.IsEmpty | IsEmpty | |
| Is.EqualTo | Equal | CollectionAssert.AreEqual | IsEquivalent (overload) | Same order |
| Is.EquivalentTo | Equivalent | CollectionAssert.AreEquivalent | IsEquivalent | Allow different order |
| Has.Some.EqualTo() | Contains | CollectionAssert.Contains | Contains | |
| Contains.Item | Contains | CollectionAssert.Contains | ||
| Is.Ordered.Descending | IsInOrder IsInDescendingOrder |
|||
| Is.All.GreaterThan(1) | All(col, x => Assert.True(x > 1)) | ContainsOnly | ||
| Has.None.Null | All | CollectionAssert .AllItemsAreNotNull |
||
| Has.Exactly(3).Items | Assert.HasCount | |||
| Is.SubsetOf | CollectionAssert.IsSubsetOf | |||
| Is.Unique | CollectionAssert .AllItemsAreUnique |
HasDistinctItems |
Specific assertions per element in the collection.
Assert.Collection(
collection,
el1 => Assert.Null(el1),
el2 => Assert.False(el2),
...
);
var dict = new Dictionary<int, int>
{
{ 1, 4 },
{ 2, 5 }
};
Assert.That(dict, Does.ContainKey(1).WithValue(4));
Assert.That(dict, Contains.Value(4));
Assert.That(dict, Does.ContainValue(5));
Assert.That(dict, Does.Not.ContainValue(3));
// xUnit
// Only InRange
Assert.InRange(actual, 0, 100);
// NUnit
Assert.That(actual, Is.InRange(0, 100));
Assert.That(actual, Is.AtMost(0));
// Also: AtLeast, Zero, LessThanOrEqualTo, Negative/Positive
// Greater/LessThan uses IComparable
// MSTest: N/A
// TUnit
await Assert.That(actual).IsBetween(0, 100);
await Assert.That(actual).IsLessThan(0);
// Also: IsGreaterThan, IsNotLessThanOrEqualTo
// Is(Not)Zero, IsNegative/Positive
A test stops running as soon as an assertion fails. But sometimes it is handy that all assertions still run so that you have more context to pinpoint the exact problem. Both xUnit and NUnit have a mechanism to do this.
// xUnit
Assert.Multiple(
() => Assert.NotEqual(12, 24),
() => Assert.NotNull("Hello world")
);
// NUnit
using (Assert.EnterMultipleScope())
{
Assert.That(12, Is.Not.EqualTo(24));
Assert.That("Hello world", Is.Not.Null);
}
// TUnit
// Both NUnit & TUnit can also use block scoped using
using var _ = Assert.Multiple();
await Assert.That(12).IsNotEqualTo(24);
await Assert.That("Hello world").IsNotNull();
Before NUnit 4.2 the syntax was: Assert.Multiple(() => { /* assertions */ })
// NUnit
Assert.That(later, Is.EqualTo(now).Within(TimeSpan.FromHours(3.0)));
Assert.That(later, Is.EqualTo(now).Within(3).Hours);
// TUnit
await Assert.That(later).IsAfter(now);
await Assert.That(later).IsBetween(now, now.AddHours(3));
Also: Is.SamePath(OrUnder)
// NUnit only
Assert.That("/folder1/./junk/../folder2", Is.SubPathOf("/folder1/"));
Assert.That(new DirectoryInfo("c:\temp"), Does.Exist);
Assert.That(new DirectoryInfo("c:\temp"), Is.Not.Empty);
// NUnit
Assert.That(3, Is.LessThan(5).Or.GreaterThan(10));
// TUnit
await Assert.That(3).IsLessThan(5).Or.IsGreaterThan(10);
TUnit: For more complicated AND/OR logic, check AssertionGroup.For
Stuff that came into being during the making of this post
Other interesting reads
shouldly/shouldly: Should testing for .NET—the way assertions should be!
NUnit: v4.3.2
xUnit: v2.9.3
MSTest.TestFramework: v3.8.2
TUnit: v0.18.9